@zhafron/opencode-kiro-auth 1.4.11 → 1.5.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/dist/constants.d.ts +0 -1
- package/dist/constants.js +0 -9
- package/dist/core/account/account-selector.d.ts +21 -0
- package/dist/core/account/account-selector.js +64 -0
- package/dist/core/account/usage-tracker.d.ts +17 -0
- package/dist/core/account/usage-tracker.js +39 -0
- package/dist/core/auth/auth-handler.d.ts +15 -0
- package/dist/core/auth/auth-handler.js +33 -0
- package/dist/core/auth/idc-auth-method.d.ts +17 -0
- package/dist/core/auth/idc-auth-method.js +194 -0
- package/dist/core/auth/token-refresher.d.ts +22 -0
- package/dist/core/auth/token-refresher.js +53 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +9 -0
- package/dist/core/request/error-handler.d.ts +29 -0
- package/dist/core/request/error-handler.js +113 -0
- package/dist/core/request/request-handler.d.ts +25 -0
- package/dist/core/request/request-handler.js +180 -0
- package/dist/core/request/response-handler.d.ts +5 -0
- package/dist/core/request/response-handler.js +61 -0
- package/dist/core/request/retry-strategy.d.ts +20 -0
- package/dist/core/request/retry-strategy.js +32 -0
- package/dist/infrastructure/database/account-cache.d.ts +14 -0
- package/dist/infrastructure/database/account-cache.js +44 -0
- package/dist/infrastructure/database/account-repository.d.ts +12 -0
- package/dist/infrastructure/database/account-repository.js +64 -0
- package/dist/infrastructure/index.d.ts +7 -0
- package/dist/infrastructure/index.js +7 -0
- package/dist/infrastructure/transformers/event-stream-parser.d.ts +7 -0
- package/dist/infrastructure/transformers/event-stream-parser.js +115 -0
- package/dist/infrastructure/transformers/history-builder.d.ts +5 -0
- package/dist/infrastructure/transformers/history-builder.js +171 -0
- package/dist/infrastructure/transformers/message-transformer.d.ts +6 -0
- package/dist/infrastructure/transformers/message-transformer.js +102 -0
- package/dist/infrastructure/transformers/tool-call-parser.d.ts +4 -0
- package/dist/infrastructure/transformers/tool-call-parser.js +45 -0
- package/dist/infrastructure/transformers/tool-transformer.d.ts +2 -0
- package/dist/infrastructure/transformers/tool-transformer.js +19 -0
- package/dist/kiro/auth.d.ts +0 -1
- package/dist/kiro/auth.js +0 -7
- package/dist/plugin/accounts.d.ts +0 -1
- package/dist/plugin/accounts.js +4 -4
- package/dist/plugin/cli.d.ts +2 -1
- package/dist/plugin/cli.js +38 -2
- package/dist/plugin/models.d.ts +0 -2
- package/dist/plugin/models.js +0 -6
- package/dist/plugin/request.d.ts +0 -2
- package/dist/plugin/request.js +5 -282
- package/dist/plugin/response.d.ts +2 -5
- package/dist/plugin/response.js +3 -161
- package/dist/plugin/storage/migrations.d.ts +2 -0
- package/dist/plugin/storage/migrations.js +109 -0
- package/dist/plugin/storage/sqlite.js +5 -56
- package/dist/plugin/streaming/index.d.ts +2 -0
- package/dist/plugin/streaming/index.js +2 -0
- package/dist/plugin/streaming/openai-converter.d.ts +2 -0
- package/dist/plugin/streaming/openai-converter.js +68 -0
- package/dist/plugin/streaming/stream-parser.d.ts +5 -0
- package/dist/plugin/streaming/stream-parser.js +136 -0
- package/dist/plugin/streaming/stream-state.d.ts +5 -0
- package/dist/plugin/streaming/stream-state.js +59 -0
- package/dist/plugin/streaming/stream-transformer.d.ts +1 -0
- package/dist/plugin/{streaming.js → streaming/stream-transformer.js} +6 -268
- package/dist/plugin/streaming/types.d.ts +25 -0
- package/dist/plugin/streaming/types.js +2 -0
- package/dist/plugin/sync/kiro-cli-parser.d.ts +8 -0
- package/dist/plugin/sync/kiro-cli-parser.js +72 -0
- package/dist/plugin/sync/kiro-cli.js +1 -73
- package/dist/plugin/types.d.ts +0 -13
- package/dist/plugin.d.ts +6 -6
- package/dist/plugin.js +16 -512
- package/package.json +11 -3
- package/dist/plugin/storage/migration.d.ts +0 -1
- package/dist/plugin/storage/migration.js +0 -54
- package/dist/plugin/streaming.d.ts +0 -3
package/dist/constants.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { KiroRegion } from './plugin/types';
|
|
|
2
2
|
export declare function isValidRegion(region: string): region is KiroRegion;
|
|
3
3
|
export declare function normalizeRegion(region: string | undefined): KiroRegion;
|
|
4
4
|
export declare function buildUrl(template: string, region: KiroRegion): string;
|
|
5
|
-
export declare function validateUrl(url: string): boolean;
|
|
6
5
|
export declare const KIRO_CONSTANTS: {
|
|
7
6
|
REFRESH_URL: string;
|
|
8
7
|
REFRESH_IDC_URL: string;
|
package/dist/constants.js
CHANGED
|
@@ -18,15 +18,6 @@ export function buildUrl(template, region) {
|
|
|
18
18
|
throw new Error(`Invalid URL generated: ${url}`);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
export function validateUrl(url) {
|
|
22
|
-
try {
|
|
23
|
-
new URL(url);
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
21
|
export const KIRO_CONSTANTS = {
|
|
31
22
|
REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken',
|
|
32
23
|
REFRESH_IDC_URL: 'https://oidc.{{region}}.amazonaws.com/token',
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AccountRepository } from '../../infrastructure/database/account-repository';
|
|
2
|
+
import type { AccountManager } from '../../plugin/accounts';
|
|
3
|
+
import type { ManagedAccount } from '../../plugin/types';
|
|
4
|
+
type ToastFunction = (message: string, variant: 'info' | 'warning' | 'success' | 'error') => void;
|
|
5
|
+
interface AccountSelectorConfig {
|
|
6
|
+
auto_sync_kiro_cli: boolean;
|
|
7
|
+
account_selection_strategy: 'sticky' | 'round-robin' | 'lowest-usage';
|
|
8
|
+
}
|
|
9
|
+
export declare class AccountSelector {
|
|
10
|
+
private accountManager;
|
|
11
|
+
private config;
|
|
12
|
+
private syncFromKiroCli;
|
|
13
|
+
private repository;
|
|
14
|
+
private triedEmptySync;
|
|
15
|
+
constructor(accountManager: AccountManager, config: AccountSelectorConfig, syncFromKiroCli: () => Promise<void>, repository: AccountRepository);
|
|
16
|
+
selectHealthyAccount(showToast: ToastFunction): Promise<ManagedAccount | null>;
|
|
17
|
+
private handleEmptyAccounts;
|
|
18
|
+
private formatUsageMessage;
|
|
19
|
+
private sleep;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export class AccountSelector {
|
|
2
|
+
accountManager;
|
|
3
|
+
config;
|
|
4
|
+
syncFromKiroCli;
|
|
5
|
+
repository;
|
|
6
|
+
triedEmptySync = false;
|
|
7
|
+
constructor(accountManager, config, syncFromKiroCli, repository) {
|
|
8
|
+
this.accountManager = accountManager;
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.syncFromKiroCli = syncFromKiroCli;
|
|
11
|
+
this.repository = repository;
|
|
12
|
+
}
|
|
13
|
+
async selectHealthyAccount(showToast) {
|
|
14
|
+
let count = this.accountManager.getAccountCount();
|
|
15
|
+
if (count === 0 && this.config.auto_sync_kiro_cli && !this.triedEmptySync) {
|
|
16
|
+
this.triedEmptySync = true;
|
|
17
|
+
await this.handleEmptyAccounts();
|
|
18
|
+
count = this.accountManager.getAccountCount();
|
|
19
|
+
}
|
|
20
|
+
if (count === 0) {
|
|
21
|
+
throw new Error('No accounts');
|
|
22
|
+
}
|
|
23
|
+
let acc = this.accountManager.getCurrentOrNext();
|
|
24
|
+
if (!acc) {
|
|
25
|
+
const wait = this.accountManager.getMinWaitTime();
|
|
26
|
+
if (wait > 0 && wait < 30000) {
|
|
27
|
+
if (this.accountManager.shouldShowToast()) {
|
|
28
|
+
showToast(`All accounts rate-limited. Waiting ${Math.ceil(wait / 1000)}s...`, 'warning');
|
|
29
|
+
}
|
|
30
|
+
await this.sleep(wait);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
throw new Error('All accounts are unhealthy or rate-limited');
|
|
34
|
+
}
|
|
35
|
+
if (this.accountManager.shouldShowToast()) {
|
|
36
|
+
showToast(`Using ${acc.email} (${this.accountManager.getAccounts().indexOf(acc) + 1}/${count})`, 'info');
|
|
37
|
+
}
|
|
38
|
+
if (this.accountManager.shouldShowUsageToast() &&
|
|
39
|
+
acc.usedCount !== undefined &&
|
|
40
|
+
acc.limitCount !== undefined) {
|
|
41
|
+
const p = acc.limitCount > 0 ? (acc.usedCount / acc.limitCount) * 100 : 0;
|
|
42
|
+
showToast(this.formatUsageMessage(acc.usedCount, acc.limitCount, acc.email), p >= 80 ? 'warning' : 'info');
|
|
43
|
+
}
|
|
44
|
+
return acc;
|
|
45
|
+
}
|
|
46
|
+
async handleEmptyAccounts() {
|
|
47
|
+
await this.syncFromKiroCli();
|
|
48
|
+
this.repository.invalidateCache();
|
|
49
|
+
const accounts = await this.repository.findAll();
|
|
50
|
+
for (const a of accounts) {
|
|
51
|
+
this.accountManager.addAccount(a);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
formatUsageMessage(usedCount, limitCount, email) {
|
|
55
|
+
if (limitCount > 0) {
|
|
56
|
+
const percentage = Math.round((usedCount / limitCount) * 100);
|
|
57
|
+
return `Usage (${email}): ${usedCount}/${limitCount} (${percentage}%)`;
|
|
58
|
+
}
|
|
59
|
+
return `Usage (${email}): ${usedCount}`;
|
|
60
|
+
}
|
|
61
|
+
sleep(ms) {
|
|
62
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AccountRepository } from '../../infrastructure/database/account-repository';
|
|
2
|
+
import type { AccountManager } from '../../plugin/accounts';
|
|
3
|
+
import type { KiroAuthDetails, ManagedAccount } from '../../plugin/types';
|
|
4
|
+
interface UsageTrackerConfig {
|
|
5
|
+
usage_tracking_enabled: boolean;
|
|
6
|
+
usage_sync_max_retries: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class UsageTracker {
|
|
9
|
+
private config;
|
|
10
|
+
private accountManager;
|
|
11
|
+
private repository;
|
|
12
|
+
constructor(config: UsageTrackerConfig, accountManager: AccountManager, repository: AccountRepository);
|
|
13
|
+
syncUsage(account: ManagedAccount, auth: KiroAuthDetails): Promise<void>;
|
|
14
|
+
private syncWithRetry;
|
|
15
|
+
private sleep;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { fetchUsageLimits, updateAccountQuota } from '../../plugin/usage';
|
|
2
|
+
export class UsageTracker {
|
|
3
|
+
config;
|
|
4
|
+
accountManager;
|
|
5
|
+
repository;
|
|
6
|
+
constructor(config, accountManager, repository) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
this.accountManager = accountManager;
|
|
9
|
+
this.repository = repository;
|
|
10
|
+
}
|
|
11
|
+
async syncUsage(account, auth) {
|
|
12
|
+
if (!this.config.usage_tracking_enabled) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
this.syncWithRetry(account, auth, 0).catch(() => { });
|
|
16
|
+
}
|
|
17
|
+
async syncWithRetry(account, auth, attempt) {
|
|
18
|
+
try {
|
|
19
|
+
const u = await fetchUsageLimits(auth);
|
|
20
|
+
updateAccountQuota(account, u, this.accountManager);
|
|
21
|
+
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
if (attempt < this.config.usage_sync_max_retries) {
|
|
25
|
+
await this.sleep(1000 * Math.pow(2, attempt));
|
|
26
|
+
return this.syncWithRetry(account, auth, attempt + 1);
|
|
27
|
+
}
|
|
28
|
+
if (e.message?.includes('403') ||
|
|
29
|
+
e.message?.includes('invalid') ||
|
|
30
|
+
e.message?.includes('bearer token')) {
|
|
31
|
+
this.accountManager.markUnhealthy(account, e.message);
|
|
32
|
+
this.repository.save(account).catch(() => { });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
sleep(ms) {
|
|
37
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AccountRepository } from '../../infrastructure/database/account-repository.js';
|
|
2
|
+
export declare class AuthHandler {
|
|
3
|
+
private config;
|
|
4
|
+
private repository;
|
|
5
|
+
private accountManager?;
|
|
6
|
+
constructor(config: any, repository: AccountRepository);
|
|
7
|
+
initialize(): Promise<void>;
|
|
8
|
+
setAccountManager(am: any): void;
|
|
9
|
+
getMethods(): Array<{
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
type: 'oauth';
|
|
13
|
+
authorize: (inputs?: any) => Promise<any>;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { IdcAuthMethod } from './idc-auth-method.js';
|
|
2
|
+
export class AuthHandler {
|
|
3
|
+
config;
|
|
4
|
+
repository;
|
|
5
|
+
accountManager;
|
|
6
|
+
constructor(config, repository) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
this.repository = repository;
|
|
9
|
+
}
|
|
10
|
+
async initialize() {
|
|
11
|
+
const { syncFromKiroCli } = await import('../../plugin/sync/kiro-cli.js');
|
|
12
|
+
if (this.config.auto_sync_kiro_cli) {
|
|
13
|
+
await syncFromKiroCli();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
setAccountManager(am) {
|
|
17
|
+
this.accountManager = am;
|
|
18
|
+
}
|
|
19
|
+
getMethods() {
|
|
20
|
+
if (!this.accountManager) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const idcMethod = new IdcAuthMethod(this.config, this.repository);
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
id: 'idc',
|
|
27
|
+
label: 'AWS Builder ID (IDC)',
|
|
28
|
+
type: 'oauth',
|
|
29
|
+
authorize: (inputs) => idcMethod.authorize(inputs)
|
|
30
|
+
}
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AccountRepository } from '../../infrastructure/database/account-repository.js';
|
|
2
|
+
export declare class IdcAuthMethod {
|
|
3
|
+
private config;
|
|
4
|
+
private repository;
|
|
5
|
+
constructor(config: any, repository: AccountRepository);
|
|
6
|
+
authorize(inputs?: any): Promise<{
|
|
7
|
+
url: string;
|
|
8
|
+
instructions: string;
|
|
9
|
+
method: 'auto';
|
|
10
|
+
callback: () => Promise<{
|
|
11
|
+
type: 'success' | 'failed';
|
|
12
|
+
key?: string;
|
|
13
|
+
}>;
|
|
14
|
+
}>;
|
|
15
|
+
private handleMultipleLogin;
|
|
16
|
+
private handleSingleLogin;
|
|
17
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { authorizeKiroIDC } from '../../kiro/oauth-idc.js';
|
|
3
|
+
import { createDeterministicAccountId } from '../../plugin/accounts.js';
|
|
4
|
+
import { promptAddAnotherAccount, promptDeleteAccount, promptLoginMode } from '../../plugin/cli.js';
|
|
5
|
+
import * as logger from '../../plugin/logger.js';
|
|
6
|
+
import { startIDCAuthServer } from '../../plugin/server.js';
|
|
7
|
+
import { fetchUsageLimits } from '../../plugin/usage.js';
|
|
8
|
+
const openBrowser = (url) => {
|
|
9
|
+
const escapedUrl = url.replace(/"/g, '\\"');
|
|
10
|
+
const platform = process.platform;
|
|
11
|
+
const cmd = platform === 'win32'
|
|
12
|
+
? `cmd /c start "" "${escapedUrl}"`
|
|
13
|
+
: platform === 'darwin'
|
|
14
|
+
? `open "${escapedUrl}"`
|
|
15
|
+
: `xdg-open "${escapedUrl}"`;
|
|
16
|
+
exec(cmd, (error) => {
|
|
17
|
+
if (error)
|
|
18
|
+
logger.warn(`Browser error: ${error.message}`);
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
export class IdcAuthMethod {
|
|
22
|
+
config;
|
|
23
|
+
repository;
|
|
24
|
+
constructor(config, repository) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.repository = repository;
|
|
27
|
+
}
|
|
28
|
+
async authorize(inputs) {
|
|
29
|
+
return new Promise(async (resolve) => {
|
|
30
|
+
const region = this.config.default_region;
|
|
31
|
+
if (inputs) {
|
|
32
|
+
await this.handleMultipleLogin(region, resolve);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
await this.handleSingleLogin(region, resolve);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async handleMultipleLogin(region, resolve) {
|
|
40
|
+
const accounts = [];
|
|
41
|
+
let startFresh = true;
|
|
42
|
+
const existingAccounts = await this.repository.findAll();
|
|
43
|
+
const idcAccs = existingAccounts.filter((a) => a.authMethod === 'idc');
|
|
44
|
+
if (idcAccs.length > 0) {
|
|
45
|
+
const existingAccountsList = idcAccs.map((acc, idx) => ({
|
|
46
|
+
email: acc.email,
|
|
47
|
+
index: idx
|
|
48
|
+
}));
|
|
49
|
+
const mode = await promptLoginMode(existingAccountsList);
|
|
50
|
+
if (mode === 'delete') {
|
|
51
|
+
const deleteIndex = await promptDeleteAccount(existingAccountsList);
|
|
52
|
+
if (deleteIndex !== null && idcAccs[deleteIndex]) {
|
|
53
|
+
const accToDelete = idcAccs[deleteIndex];
|
|
54
|
+
await this.repository.delete(accToDelete.id);
|
|
55
|
+
console.log(`\n[Success] Deleted: ${accToDelete.email}\n`);
|
|
56
|
+
}
|
|
57
|
+
const finalAccounts = await this.repository.findAll();
|
|
58
|
+
return resolve({
|
|
59
|
+
url: '',
|
|
60
|
+
instructions: `Complete (${finalAccounts.length} accounts).`,
|
|
61
|
+
method: 'auto',
|
|
62
|
+
callback: async () => ({
|
|
63
|
+
type: 'success',
|
|
64
|
+
key: finalAccounts[0]?.accessToken || ''
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
startFresh = mode === 'fresh';
|
|
69
|
+
}
|
|
70
|
+
while (true) {
|
|
71
|
+
try {
|
|
72
|
+
const authData = await authorizeKiroIDC(region);
|
|
73
|
+
const { url, waitForAuth } = await startIDCAuthServer(authData, this.config.auth_server_port_start, this.config.auth_server_port_range);
|
|
74
|
+
openBrowser(url);
|
|
75
|
+
const res = await waitForAuth();
|
|
76
|
+
const u = await fetchUsageLimits({
|
|
77
|
+
refresh: '',
|
|
78
|
+
access: res.accessToken,
|
|
79
|
+
expires: res.expiresAt,
|
|
80
|
+
authMethod: 'idc',
|
|
81
|
+
region,
|
|
82
|
+
clientId: res.clientId,
|
|
83
|
+
clientSecret: res.clientSecret
|
|
84
|
+
});
|
|
85
|
+
if (!u.email) {
|
|
86
|
+
console.log('\n[Error] Failed to fetch account email. Skipping...\n');
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
accounts.push(res);
|
|
90
|
+
if (accounts.length === 1 && startFresh) {
|
|
91
|
+
const allAccounts = await this.repository.findAll();
|
|
92
|
+
const idcAccountsToRemove = allAccounts.filter((a) => a.authMethod === 'idc');
|
|
93
|
+
for (const acc of idcAccountsToRemove) {
|
|
94
|
+
await this.repository.delete(acc.id);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
|
|
98
|
+
const acc = {
|
|
99
|
+
id,
|
|
100
|
+
email: u.email,
|
|
101
|
+
authMethod: 'idc',
|
|
102
|
+
region,
|
|
103
|
+
clientId: res.clientId,
|
|
104
|
+
clientSecret: res.clientSecret,
|
|
105
|
+
refreshToken: res.refreshToken,
|
|
106
|
+
accessToken: res.accessToken,
|
|
107
|
+
expiresAt: res.expiresAt,
|
|
108
|
+
rateLimitResetTime: 0,
|
|
109
|
+
isHealthy: true,
|
|
110
|
+
failCount: 0,
|
|
111
|
+
usedCount: u.usedCount,
|
|
112
|
+
limitCount: u.limitCount
|
|
113
|
+
};
|
|
114
|
+
await this.repository.save(acc);
|
|
115
|
+
const currentCount = (await this.repository.findAll()).length;
|
|
116
|
+
console.log(`\n[Success] Added: ${u.email} (Quota: ${u.usedCount}/${u.limitCount})\n`);
|
|
117
|
+
if (!(await promptAddAnotherAccount(currentCount)))
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
console.log(`\n[Error] Login failed: ${e.message}\n`);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const finalAccounts = await this.repository.findAll();
|
|
126
|
+
return resolve({
|
|
127
|
+
url: '',
|
|
128
|
+
instructions: `Complete (${finalAccounts.length} accounts).`,
|
|
129
|
+
method: 'auto',
|
|
130
|
+
callback: async () => ({
|
|
131
|
+
type: 'success',
|
|
132
|
+
key: finalAccounts[0]?.accessToken || ''
|
|
133
|
+
})
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async handleSingleLogin(region, resolve) {
|
|
137
|
+
try {
|
|
138
|
+
const authData = await authorizeKiroIDC(region);
|
|
139
|
+
const { url, waitForAuth } = await startIDCAuthServer(authData, this.config.auth_server_port_start, this.config.auth_server_port_range);
|
|
140
|
+
openBrowser(url);
|
|
141
|
+
resolve({
|
|
142
|
+
url,
|
|
143
|
+
instructions: `Open: ${url}`,
|
|
144
|
+
method: 'auto',
|
|
145
|
+
callback: async () => {
|
|
146
|
+
try {
|
|
147
|
+
const res = await waitForAuth();
|
|
148
|
+
const u = await fetchUsageLimits({
|
|
149
|
+
refresh: '',
|
|
150
|
+
access: res.accessToken,
|
|
151
|
+
expires: res.expiresAt,
|
|
152
|
+
authMethod: 'idc',
|
|
153
|
+
region,
|
|
154
|
+
clientId: res.clientId,
|
|
155
|
+
clientSecret: res.clientSecret
|
|
156
|
+
});
|
|
157
|
+
if (!u.email)
|
|
158
|
+
throw new Error('No email');
|
|
159
|
+
const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
|
|
160
|
+
const acc = {
|
|
161
|
+
id,
|
|
162
|
+
email: u.email,
|
|
163
|
+
authMethod: 'idc',
|
|
164
|
+
region,
|
|
165
|
+
clientId: res.clientId,
|
|
166
|
+
clientSecret: res.clientSecret,
|
|
167
|
+
refreshToken: res.refreshToken,
|
|
168
|
+
accessToken: res.accessToken,
|
|
169
|
+
expiresAt: res.expiresAt,
|
|
170
|
+
rateLimitResetTime: 0,
|
|
171
|
+
isHealthy: true,
|
|
172
|
+
failCount: 0,
|
|
173
|
+
usedCount: u.usedCount,
|
|
174
|
+
limitCount: u.limitCount
|
|
175
|
+
};
|
|
176
|
+
await this.repository.save(acc);
|
|
177
|
+
return { type: 'success', key: res.accessToken };
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
return { type: 'failed' };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
resolve({
|
|
187
|
+
url: '',
|
|
188
|
+
instructions: 'Failed',
|
|
189
|
+
method: 'auto',
|
|
190
|
+
callback: async () => ({ type: 'failed' })
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AccountRepository } from '../../infrastructure/database/account-repository';
|
|
2
|
+
import type { AccountManager } from '../../plugin/accounts';
|
|
3
|
+
import type { KiroAuthDetails, ManagedAccount } from '../../plugin/types';
|
|
4
|
+
type ToastFunction = (message: string, variant: 'info' | 'warning' | 'success' | 'error') => void;
|
|
5
|
+
interface TokenRefresherConfig {
|
|
6
|
+
token_expiry_buffer_ms: number;
|
|
7
|
+
auto_sync_kiro_cli: boolean;
|
|
8
|
+
account_selection_strategy: 'sticky' | 'round-robin' | 'lowest-usage';
|
|
9
|
+
}
|
|
10
|
+
export declare class TokenRefresher {
|
|
11
|
+
private config;
|
|
12
|
+
private accountManager;
|
|
13
|
+
private syncFromKiroCli;
|
|
14
|
+
private repository;
|
|
15
|
+
constructor(config: TokenRefresherConfig, accountManager: AccountManager, syncFromKiroCli: () => Promise<void>, repository: AccountRepository);
|
|
16
|
+
refreshIfNeeded(account: ManagedAccount, auth: KiroAuthDetails, showToast: ToastFunction): Promise<{
|
|
17
|
+
account: ManagedAccount;
|
|
18
|
+
shouldContinue: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
private handleRefreshError;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { accessTokenExpired } from '../../kiro/auth';
|
|
2
|
+
import { KiroTokenRefreshError } from '../../plugin/errors';
|
|
3
|
+
import { refreshAccessToken } from '../../plugin/token';
|
|
4
|
+
export class TokenRefresher {
|
|
5
|
+
config;
|
|
6
|
+
accountManager;
|
|
7
|
+
syncFromKiroCli;
|
|
8
|
+
repository;
|
|
9
|
+
constructor(config, accountManager, syncFromKiroCli, repository) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.accountManager = accountManager;
|
|
12
|
+
this.syncFromKiroCli = syncFromKiroCli;
|
|
13
|
+
this.repository = repository;
|
|
14
|
+
}
|
|
15
|
+
async refreshIfNeeded(account, auth, showToast) {
|
|
16
|
+
if (!accessTokenExpired(auth, this.config.token_expiry_buffer_ms)) {
|
|
17
|
+
return { account, shouldContinue: false };
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const newAuth = await refreshAccessToken(auth);
|
|
21
|
+
this.accountManager.updateFromAuth(account, newAuth);
|
|
22
|
+
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
23
|
+
return { account, shouldContinue: false };
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
return await this.handleRefreshError(e, account, showToast);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async handleRefreshError(error, account, showToast) {
|
|
30
|
+
if (this.config.auto_sync_kiro_cli) {
|
|
31
|
+
await this.syncFromKiroCli();
|
|
32
|
+
}
|
|
33
|
+
this.repository.invalidateCache();
|
|
34
|
+
const accounts = await this.repository.findAll();
|
|
35
|
+
const stillAcc = accounts.find((a) => a.id === account.id);
|
|
36
|
+
if (stillAcc &&
|
|
37
|
+
!accessTokenExpired(this.accountManager.toAuthDetails(stillAcc), this.config.token_expiry_buffer_ms)) {
|
|
38
|
+
showToast('Credentials recovered from Kiro CLI sync.', 'info');
|
|
39
|
+
return { account: stillAcc, shouldContinue: true };
|
|
40
|
+
}
|
|
41
|
+
if (error instanceof KiroTokenRefreshError &&
|
|
42
|
+
(error.code === 'ExpiredTokenException' ||
|
|
43
|
+
error.code === 'InvalidTokenException' ||
|
|
44
|
+
error.code === 'HTTP_401' ||
|
|
45
|
+
error.code === 'HTTP_403' ||
|
|
46
|
+
error.message.includes('Invalid refresh token provided'))) {
|
|
47
|
+
this.accountManager.markUnhealthy(account, error.message);
|
|
48
|
+
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
49
|
+
return { account, shouldContinue: true };
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './account/account-selector.js';
|
|
2
|
+
export * from './account/usage-tracker.js';
|
|
3
|
+
export * from './auth/auth-handler.js';
|
|
4
|
+
export * from './auth/idc-auth-method.js';
|
|
5
|
+
export * from './auth/token-refresher.js';
|
|
6
|
+
export * from './request/error-handler.js';
|
|
7
|
+
export * from './request/request-handler.js';
|
|
8
|
+
export * from './request/response-handler.js';
|
|
9
|
+
export * from './request/retry-strategy.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './account/account-selector.js';
|
|
2
|
+
export * from './account/usage-tracker.js';
|
|
3
|
+
export * from './auth/auth-handler.js';
|
|
4
|
+
export * from './auth/idc-auth-method.js';
|
|
5
|
+
export * from './auth/token-refresher.js';
|
|
6
|
+
export * from './request/error-handler.js';
|
|
7
|
+
export * from './request/request-handler.js';
|
|
8
|
+
export * from './request/response-handler.js';
|
|
9
|
+
export * from './request/retry-strategy.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AccountRepository } from '../../infrastructure/database/account-repository';
|
|
2
|
+
import type { AccountManager } from '../../plugin/accounts';
|
|
3
|
+
import type { ManagedAccount } from '../../plugin/types';
|
|
4
|
+
type ToastFunction = (message: string, variant: 'info' | 'warning' | 'success' | 'error') => void;
|
|
5
|
+
interface RequestContext {
|
|
6
|
+
reductionFactor: number;
|
|
7
|
+
retry: number;
|
|
8
|
+
}
|
|
9
|
+
interface ErrorHandlerConfig {
|
|
10
|
+
rate_limit_max_retries: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class ErrorHandler {
|
|
13
|
+
private config;
|
|
14
|
+
private accountManager;
|
|
15
|
+
private repository;
|
|
16
|
+
constructor(config: ErrorHandlerConfig, accountManager: AccountManager, repository: AccountRepository);
|
|
17
|
+
handle(error: any, response: Response, account: ManagedAccount, context: RequestContext, showToast: ToastFunction): Promise<{
|
|
18
|
+
shouldRetry: boolean;
|
|
19
|
+
newContext?: RequestContext;
|
|
20
|
+
switchAccount?: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
handleNetworkError(error: any, context: RequestContext, showToast: ToastFunction): Promise<{
|
|
23
|
+
shouldRetry: boolean;
|
|
24
|
+
newContext?: RequestContext;
|
|
25
|
+
}>;
|
|
26
|
+
private isNetworkError;
|
|
27
|
+
private sleep;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export class ErrorHandler {
|
|
2
|
+
config;
|
|
3
|
+
accountManager;
|
|
4
|
+
repository;
|
|
5
|
+
constructor(config, accountManager, repository) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.accountManager = accountManager;
|
|
8
|
+
this.repository = repository;
|
|
9
|
+
}
|
|
10
|
+
async handle(error, response, account, context, showToast) {
|
|
11
|
+
if (response.status === 400 && context.reductionFactor > 0.4) {
|
|
12
|
+
const newFactor = context.reductionFactor - 0.2;
|
|
13
|
+
showToast(`Context too long. Retrying with ${Math.round(newFactor * 100)}%...`, 'warning');
|
|
14
|
+
return {
|
|
15
|
+
shouldRetry: true,
|
|
16
|
+
newContext: { ...context, reductionFactor: newFactor }
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (response.status === 401 && context.retry < this.config.rate_limit_max_retries) {
|
|
20
|
+
return {
|
|
21
|
+
shouldRetry: true,
|
|
22
|
+
newContext: { ...context, retry: context.retry + 1 }
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (response.status === 500) {
|
|
26
|
+
account.failCount = (account.failCount || 0) + 1;
|
|
27
|
+
let errorMessage = 'Internal Server Error';
|
|
28
|
+
try {
|
|
29
|
+
const errorBody = await response.text();
|
|
30
|
+
const errorData = JSON.parse(errorBody);
|
|
31
|
+
if (errorData.message) {
|
|
32
|
+
errorMessage = errorData.message;
|
|
33
|
+
}
|
|
34
|
+
else if (errorData.Message) {
|
|
35
|
+
errorMessage = errorData.Message;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (e) { }
|
|
39
|
+
if (account.failCount < 5) {
|
|
40
|
+
const delay = 1000 * Math.pow(2, account.failCount - 1);
|
|
41
|
+
showToast(`Server Error (500): ${errorMessage}. Retrying in ${Math.ceil(delay / 1000)}s...`, 'warning');
|
|
42
|
+
await this.sleep(delay);
|
|
43
|
+
return { shouldRetry: true };
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
this.accountManager.markUnhealthy(account, `Server Error (500) after 5 attempts: ${errorMessage}`);
|
|
47
|
+
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
48
|
+
showToast(`Server Error (500): ${errorMessage}. Marking account as unhealthy and switching...`, 'warning');
|
|
49
|
+
return { shouldRetry: true, switchAccount: true };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (response.status === 429) {
|
|
53
|
+
const w = parseInt(response.headers.get('retry-after') || '60') * 1000;
|
|
54
|
+
this.accountManager.markRateLimited(account, w);
|
|
55
|
+
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
56
|
+
const count = this.accountManager.getAccountCount();
|
|
57
|
+
if (count > 1) {
|
|
58
|
+
showToast(`Rate limited (${account.email}). Switching account...`, 'warning');
|
|
59
|
+
return { shouldRetry: true, switchAccount: true };
|
|
60
|
+
}
|
|
61
|
+
showToast(`Rate limited. Waiting ${Math.ceil(w / 1000)}s...`, 'warning');
|
|
62
|
+
await this.sleep(w);
|
|
63
|
+
return { shouldRetry: true };
|
|
64
|
+
}
|
|
65
|
+
if ((response.status === 402 || response.status === 403) &&
|
|
66
|
+
this.accountManager.getAccountCount() > 1) {
|
|
67
|
+
let errorReason = response.status === 402 ? 'Quota' : 'Forbidden';
|
|
68
|
+
let isPermanent = false;
|
|
69
|
+
try {
|
|
70
|
+
const errorBody = await response.text();
|
|
71
|
+
const errorData = JSON.parse(errorBody);
|
|
72
|
+
if (errorData.reason === 'INVALID_MODEL_ID') {
|
|
73
|
+
throw new Error(`Invalid model: ${errorData.message}`);
|
|
74
|
+
}
|
|
75
|
+
if (errorData.reason === 'TEMPORARILY_SUSPENDED') {
|
|
76
|
+
errorReason = 'Account Suspended';
|
|
77
|
+
isPermanent = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (e instanceof Error && e.message.includes('Invalid model')) {
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (isPermanent) {
|
|
86
|
+
account.failCount = 10;
|
|
87
|
+
}
|
|
88
|
+
this.accountManager.markUnhealthy(account, errorReason);
|
|
89
|
+
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
90
|
+
showToast(`${errorReason} (${account.email}). Switching account...`, 'warning');
|
|
91
|
+
return { shouldRetry: true, switchAccount: true };
|
|
92
|
+
}
|
|
93
|
+
return { shouldRetry: false };
|
|
94
|
+
}
|
|
95
|
+
async handleNetworkError(error, context, showToast) {
|
|
96
|
+
if (this.isNetworkError(error) && context.retry < this.config.rate_limit_max_retries) {
|
|
97
|
+
const d = 5000 * Math.pow(2, context.retry);
|
|
98
|
+
showToast(`Network error. Retrying in ${Math.ceil(d / 1000)}s...`, 'warning');
|
|
99
|
+
await this.sleep(d);
|
|
100
|
+
return {
|
|
101
|
+
shouldRetry: true,
|
|
102
|
+
newContext: { ...context, retry: context.retry + 1 }
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { shouldRetry: false };
|
|
106
|
+
}
|
|
107
|
+
isNetworkError(e) {
|
|
108
|
+
return (e instanceof Error && /econnreset|etimedout|enotfound|network|fetch failed/i.test(e.message));
|
|
109
|
+
}
|
|
110
|
+
sleep(ms) {
|
|
111
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
112
|
+
}
|
|
113
|
+
}
|