@kervnet/opencode-kiro-auth 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +159 -0
  2. package/dist/constants.d.ts +24 -0
  3. package/dist/constants.js +55 -0
  4. package/dist/core/account/account-selector.d.ts +25 -0
  5. package/dist/core/account/account-selector.js +84 -0
  6. package/dist/core/account/usage-tracker.d.ts +17 -0
  7. package/dist/core/account/usage-tracker.js +39 -0
  8. package/dist/core/auth/auth-handler.d.ts +15 -0
  9. package/dist/core/auth/auth-handler.js +43 -0
  10. package/dist/core/auth/idc-auth-method.d.ts +17 -0
  11. package/dist/core/auth/idc-auth-method.js +200 -0
  12. package/dist/core/auth/token-refresher.d.ts +22 -0
  13. package/dist/core/auth/token-refresher.js +53 -0
  14. package/dist/core/index.d.ts +9 -0
  15. package/dist/core/index.js +9 -0
  16. package/dist/core/request/error-handler.d.ts +30 -0
  17. package/dist/core/request/error-handler.js +113 -0
  18. package/dist/core/request/request-handler.d.ts +27 -0
  19. package/dist/core/request/request-handler.js +199 -0
  20. package/dist/core/request/response-handler.d.ts +5 -0
  21. package/dist/core/request/response-handler.js +61 -0
  22. package/dist/core/request/retry-strategy.d.ts +18 -0
  23. package/dist/core/request/retry-strategy.js +28 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.js +1 -0
  26. package/dist/infrastructure/database/account-cache.d.ts +14 -0
  27. package/dist/infrastructure/database/account-cache.js +44 -0
  28. package/dist/infrastructure/database/account-repository.d.ts +12 -0
  29. package/dist/infrastructure/database/account-repository.js +64 -0
  30. package/dist/infrastructure/index.d.ts +7 -0
  31. package/dist/infrastructure/index.js +7 -0
  32. package/dist/infrastructure/transformers/event-stream-parser.d.ts +7 -0
  33. package/dist/infrastructure/transformers/event-stream-parser.js +115 -0
  34. package/dist/infrastructure/transformers/history-builder.d.ts +5 -0
  35. package/dist/infrastructure/transformers/history-builder.js +171 -0
  36. package/dist/infrastructure/transformers/message-transformer.d.ts +6 -0
  37. package/dist/infrastructure/transformers/message-transformer.js +102 -0
  38. package/dist/infrastructure/transformers/tool-call-parser.d.ts +4 -0
  39. package/dist/infrastructure/transformers/tool-call-parser.js +45 -0
  40. package/dist/infrastructure/transformers/tool-transformer.d.ts +2 -0
  41. package/dist/infrastructure/transformers/tool-transformer.js +19 -0
  42. package/dist/kiro/auth.d.ts +4 -0
  43. package/dist/kiro/auth.js +25 -0
  44. package/dist/kiro/oauth-idc.d.ts +24 -0
  45. package/dist/kiro/oauth-idc.js +151 -0
  46. package/dist/plugin/accounts.d.ts +29 -0
  47. package/dist/plugin/accounts.js +235 -0
  48. package/dist/plugin/auth-page.d.ts +3 -0
  49. package/dist/plugin/auth-page.js +573 -0
  50. package/dist/plugin/cli.d.ts +8 -0
  51. package/dist/plugin/cli.js +103 -0
  52. package/dist/plugin/config/index.d.ts +3 -0
  53. package/dist/plugin/config/index.js +2 -0
  54. package/dist/plugin/config/loader.d.ts +6 -0
  55. package/dist/plugin/config/loader.js +129 -0
  56. package/dist/plugin/config/schema.d.ts +56 -0
  57. package/dist/plugin/config/schema.js +36 -0
  58. package/dist/plugin/errors.d.ts +17 -0
  59. package/dist/plugin/errors.js +34 -0
  60. package/dist/plugin/health.d.ts +1 -0
  61. package/dist/plugin/health.js +9 -0
  62. package/dist/plugin/image-handler.d.ts +14 -0
  63. package/dist/plugin/image-handler.js +64 -0
  64. package/dist/plugin/logger.d.ts +8 -0
  65. package/dist/plugin/logger.js +63 -0
  66. package/dist/plugin/models.d.ts +1 -0
  67. package/dist/plugin/models.js +8 -0
  68. package/dist/plugin/request.d.ts +2 -0
  69. package/dist/plugin/request.js +239 -0
  70. package/dist/plugin/response.d.ts +3 -0
  71. package/dist/plugin/response.js +95 -0
  72. package/dist/plugin/server.d.ts +24 -0
  73. package/dist/plugin/server.js +166 -0
  74. package/dist/plugin/storage/locked-operations.d.ts +5 -0
  75. package/dist/plugin/storage/locked-operations.js +91 -0
  76. package/dist/plugin/storage/migrations.d.ts +2 -0
  77. package/dist/plugin/storage/migrations.js +109 -0
  78. package/dist/plugin/storage/sqlite.d.ts +17 -0
  79. package/dist/plugin/storage/sqlite.js +134 -0
  80. package/dist/plugin/streaming/index.d.ts +2 -0
  81. package/dist/plugin/streaming/index.js +2 -0
  82. package/dist/plugin/streaming/openai-converter.d.ts +2 -0
  83. package/dist/plugin/streaming/openai-converter.js +68 -0
  84. package/dist/plugin/streaming/stream-parser.d.ts +5 -0
  85. package/dist/plugin/streaming/stream-parser.js +136 -0
  86. package/dist/plugin/streaming/stream-state.d.ts +5 -0
  87. package/dist/plugin/streaming/stream-state.js +59 -0
  88. package/dist/plugin/streaming/stream-transformer.d.ts +1 -0
  89. package/dist/plugin/streaming/stream-transformer.js +248 -0
  90. package/dist/plugin/streaming/types.d.ts +25 -0
  91. package/dist/plugin/streaming/types.js +2 -0
  92. package/dist/plugin/sync/aws-sso.d.ts +2 -0
  93. package/dist/plugin/sync/aws-sso.js +50 -0
  94. package/dist/plugin/sync/kiro-cli-parser.d.ts +8 -0
  95. package/dist/plugin/sync/kiro-cli-parser.js +72 -0
  96. package/dist/plugin/sync/kiro-cli.d.ts +2 -0
  97. package/dist/plugin/sync/kiro-cli.js +197 -0
  98. package/dist/plugin/token.d.ts +2 -0
  99. package/dist/plugin/token.js +79 -0
  100. package/dist/plugin/types.d.ts +109 -0
  101. package/dist/plugin/types.js +0 -0
  102. package/dist/plugin/usage.d.ts +3 -0
  103. package/dist/plugin/usage.js +45 -0
  104. package/dist/plugin.d.ts +32 -0
  105. package/dist/plugin.js +37 -0
  106. package/package.json +65 -0
@@ -0,0 +1,4 @@
1
+ import type { ToolCall } from '../../plugin/types';
2
+ export declare function parseBracketToolCalls(text: string): ToolCall[];
3
+ export declare function deduplicateToolCalls(toolCalls: ToolCall[]): ToolCall[];
4
+ export declare function cleanToolCallsFromText(text: string, toolCalls: ToolCall[]): string;
@@ -0,0 +1,45 @@
1
+ export function parseBracketToolCalls(text) {
2
+ const toolCalls = [];
3
+ const pattern = /\[Called\s+(\w+)\s+with\s+args:\s*(\{[^}]*(?:\{[^}]*\}[^}]*)*\})\]/gs;
4
+ let match;
5
+ while ((match = pattern.exec(text)) !== null) {
6
+ const funcName = match[1];
7
+ const argsStr = match[2];
8
+ if (!funcName || !argsStr)
9
+ continue;
10
+ try {
11
+ const args = JSON.parse(argsStr);
12
+ toolCalls.push({
13
+ toolUseId: `tool_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
14
+ name: funcName,
15
+ input: args
16
+ });
17
+ }
18
+ catch (e) {
19
+ continue;
20
+ }
21
+ }
22
+ return toolCalls;
23
+ }
24
+ export function deduplicateToolCalls(toolCalls) {
25
+ const seen = new Set();
26
+ const unique = [];
27
+ for (const tc of toolCalls) {
28
+ if (!seen.has(tc.toolUseId)) {
29
+ seen.add(tc.toolUseId);
30
+ unique.push(tc);
31
+ }
32
+ }
33
+ return unique;
34
+ }
35
+ export function cleanToolCallsFromText(text, toolCalls) {
36
+ let cleaned = text;
37
+ for (const tc of toolCalls) {
38
+ const funcName = tc.name;
39
+ const escapedName = funcName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
40
+ const pattern = new RegExp(`\\[Called\\s+${escapedName}\\s+with\\s+args:\\s*\\{[^}]*(?:\\{[^}]*\\}[^}]*)*\\}\\]`, 'gs');
41
+ cleaned = cleaned.replace(pattern, '');
42
+ }
43
+ cleaned = cleaned.replace(/\s+/g, ' ').trim();
44
+ return cleaned;
45
+ }
@@ -0,0 +1,2 @@
1
+ export declare function convertToolsToCodeWhisperer(tools: any[]): any[];
2
+ export declare function deduplicateToolResults(trs: any[]): any[];
@@ -0,0 +1,19 @@
1
+ export function convertToolsToCodeWhisperer(tools) {
2
+ return tools.map((t) => ({
3
+ toolSpecification: {
4
+ name: t.name || t.function?.name,
5
+ description: (t.description || t.function?.description || '').substring(0, 9216),
6
+ inputSchema: { json: t.input_schema || t.function?.parameters || {} }
7
+ }
8
+ }));
9
+ }
10
+ export function deduplicateToolResults(trs) {
11
+ const u = [], s = new Set();
12
+ for (const t of trs) {
13
+ if (!s.has(t.toolUseId)) {
14
+ s.add(t.toolUseId);
15
+ u.push(t);
16
+ }
17
+ }
18
+ return u;
19
+ }
@@ -0,0 +1,4 @@
1
+ import type { KiroAuthDetails, RefreshParts } from '../plugin/types';
2
+ export declare function decodeRefreshToken(refresh: string): RefreshParts;
3
+ export declare function accessTokenExpired(auth: KiroAuthDetails, bufferMs?: number): boolean;
4
+ export declare function encodeRefreshToken(parts: RefreshParts): string;
@@ -0,0 +1,25 @@
1
+ export function decodeRefreshToken(refresh) {
2
+ const parts = refresh.split('|');
3
+ if (parts.length < 2)
4
+ return { refreshToken: parts[0], authMethod: 'desktop' };
5
+ const refreshToken = parts[0];
6
+ const authMethod = parts[parts.length - 1];
7
+ if (authMethod === 'idc')
8
+ return { refreshToken, clientId: parts[1], clientSecret: parts[2], authMethod: 'idc' };
9
+ if (authMethod === 'desktop')
10
+ return { refreshToken, authMethod: 'desktop' };
11
+ return { refreshToken, authMethod: 'desktop' };
12
+ }
13
+ export function accessTokenExpired(auth, bufferMs = 120000) {
14
+ if (!auth.access || !auth.expires)
15
+ return true;
16
+ return Date.now() >= auth.expires - bufferMs;
17
+ }
18
+ export function encodeRefreshToken(parts) {
19
+ if (parts.authMethod === 'idc') {
20
+ if (!parts.clientId || !parts.clientSecret)
21
+ throw new Error('Missing credentials');
22
+ return `${parts.refreshToken}|${parts.clientId}|${parts.clientSecret}|idc`;
23
+ }
24
+ return `${parts.refreshToken}|desktop`;
25
+ }
@@ -0,0 +1,24 @@
1
+ import type { KiroRegion } from '../plugin/types';
2
+ export interface KiroIDCAuthorization {
3
+ verificationUrl: string;
4
+ verificationUriComplete: string;
5
+ userCode: string;
6
+ deviceCode: string;
7
+ clientId: string;
8
+ clientSecret: string;
9
+ interval: number;
10
+ expiresIn: number;
11
+ region: KiroRegion;
12
+ }
13
+ export interface KiroIDCTokenResult {
14
+ refreshToken: string;
15
+ accessToken: string;
16
+ expiresAt: number;
17
+ email: string;
18
+ clientId: string;
19
+ clientSecret: string;
20
+ region: KiroRegion;
21
+ authMethod: 'idc';
22
+ }
23
+ export declare function authorizeKiroIDC(region?: KiroRegion): Promise<KiroIDCAuthorization>;
24
+ export declare function pollKiroIDCToken(clientId: string, clientSecret: string, deviceCode: string, interval: number, expiresIn: number, region: KiroRegion): Promise<KiroIDCTokenResult>;
@@ -0,0 +1,151 @@
1
+ import { KIRO_AUTH_SERVICE, KIRO_CONSTANTS, buildUrl, normalizeRegion } from '../constants';
2
+ export async function authorizeKiroIDC(region) {
3
+ const effectiveRegion = normalizeRegion(region);
4
+ const ssoOIDCEndpoint = buildUrl(KIRO_AUTH_SERVICE.SSO_OIDC_ENDPOINT, effectiveRegion);
5
+ try {
6
+ const registerResponse = await fetch(`${ssoOIDCEndpoint}/client/register`, {
7
+ method: 'POST',
8
+ headers: {
9
+ 'Content-Type': 'application/json',
10
+ 'User-Agent': KIRO_CONSTANTS.USER_AGENT
11
+ },
12
+ body: JSON.stringify({
13
+ clientName: 'Kiro IDE',
14
+ clientType: 'public',
15
+ scopes: KIRO_AUTH_SERVICE.SCOPES,
16
+ grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token']
17
+ })
18
+ });
19
+ if (!registerResponse.ok) {
20
+ const errorText = await registerResponse.text().catch(() => '');
21
+ const error = new Error(`Client registration failed: ${registerResponse.status} ${errorText}`);
22
+ throw error;
23
+ }
24
+ const registerData = await registerResponse.json();
25
+ const { clientId, clientSecret } = registerData;
26
+ if (!clientId || !clientSecret) {
27
+ const error = new Error('Client registration response missing clientId or clientSecret');
28
+ throw error;
29
+ }
30
+ const deviceAuthResponse = await fetch(`${ssoOIDCEndpoint}/device_authorization`, {
31
+ method: 'POST',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ 'User-Agent': KIRO_CONSTANTS.USER_AGENT
35
+ },
36
+ body: JSON.stringify({
37
+ clientId,
38
+ clientSecret,
39
+ startUrl: KIRO_AUTH_SERVICE.BUILDER_ID_START_URL
40
+ })
41
+ });
42
+ if (!deviceAuthResponse.ok) {
43
+ const errorText = await deviceAuthResponse.text().catch(() => '');
44
+ const error = new Error(`Device authorization failed: ${deviceAuthResponse.status} ${errorText}`);
45
+ throw error;
46
+ }
47
+ const deviceAuthData = await deviceAuthResponse.json();
48
+ const { verificationUri, verificationUriComplete, userCode, deviceCode, interval = 5, expiresIn = 600 } = deviceAuthData;
49
+ if (!deviceCode || !userCode || !verificationUri || !verificationUriComplete) {
50
+ const error = new Error('Device authorization response missing required fields');
51
+ throw error;
52
+ }
53
+ return {
54
+ verificationUrl: verificationUri,
55
+ verificationUriComplete,
56
+ userCode,
57
+ deviceCode,
58
+ clientId,
59
+ clientSecret,
60
+ interval,
61
+ expiresIn,
62
+ region: effectiveRegion
63
+ };
64
+ }
65
+ catch (error) {
66
+ throw error;
67
+ }
68
+ }
69
+ export async function pollKiroIDCToken(clientId, clientSecret, deviceCode, interval, expiresIn, region) {
70
+ if (!clientId || !clientSecret || !deviceCode) {
71
+ const error = new Error('Missing required parameters for token polling');
72
+ throw error;
73
+ }
74
+ const effectiveRegion = normalizeRegion(region);
75
+ const ssoOIDCEndpoint = buildUrl(KIRO_AUTH_SERVICE.SSO_OIDC_ENDPOINT, effectiveRegion);
76
+ const maxAttempts = Math.floor(expiresIn / interval);
77
+ let currentInterval = interval * 1000;
78
+ let attempts = 0;
79
+ while (attempts < maxAttempts) {
80
+ attempts++;
81
+ await new Promise((resolve) => setTimeout(resolve, currentInterval));
82
+ try {
83
+ const tokenResponse = await fetch(`${ssoOIDCEndpoint}/token`, {
84
+ method: 'POST',
85
+ headers: {
86
+ 'Content-Type': 'application/json',
87
+ 'User-Agent': KIRO_CONSTANTS.USER_AGENT
88
+ },
89
+ body: JSON.stringify({
90
+ clientId,
91
+ clientSecret,
92
+ deviceCode,
93
+ grantType: 'urn:ietf:params:oauth:grant-type:device_code'
94
+ })
95
+ });
96
+ const tokenData = await tokenResponse.json();
97
+ if (tokenData.error) {
98
+ const errorType = tokenData.error;
99
+ if (errorType === 'authorization_pending') {
100
+ continue;
101
+ }
102
+ if (errorType === 'slow_down') {
103
+ currentInterval += 5000;
104
+ continue;
105
+ }
106
+ if (errorType === 'expired_token') {
107
+ const error = new Error('Device code has expired. Please restart the authorization process.');
108
+ throw error;
109
+ }
110
+ if (errorType === 'access_denied') {
111
+ const error = new Error('Authorization was denied by the user.');
112
+ throw error;
113
+ }
114
+ const error = new Error(`Token polling failed: ${errorType} - ${tokenData.error_description || ''}`);
115
+ throw error;
116
+ }
117
+ if (tokenData.accessToken && tokenData.refreshToken) {
118
+ const expiresInSeconds = tokenData.expiresIn || 3600;
119
+ const expiresAt = Date.now() + expiresInSeconds * 1000;
120
+ return {
121
+ refreshToken: tokenData.refreshToken,
122
+ accessToken: tokenData.accessToken,
123
+ expiresAt,
124
+ email: 'builder-id@aws.amazon.com',
125
+ clientId,
126
+ clientSecret,
127
+ region: effectiveRegion,
128
+ authMethod: 'idc'
129
+ };
130
+ }
131
+ if (!tokenResponse.ok) {
132
+ const error = new Error(`Token request failed with status: ${tokenResponse.status}`);
133
+ throw error;
134
+ }
135
+ }
136
+ catch (error) {
137
+ if (error instanceof Error &&
138
+ (error.message.includes('expired') ||
139
+ error.message.includes('denied') ||
140
+ error.message.includes('failed'))) {
141
+ throw error;
142
+ }
143
+ if (attempts >= maxAttempts) {
144
+ const finalError = new Error(`Token polling failed after ${attempts} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`);
145
+ throw finalError;
146
+ }
147
+ }
148
+ }
149
+ const timeoutError = new Error('Token polling timed out. Authorization may have expired.');
150
+ throw timeoutError;
151
+ }
@@ -0,0 +1,29 @@
1
+ import type { AccountSelectionStrategy, KiroAuthDetails, ManagedAccount } from './types';
2
+ export declare function createDeterministicAccountId(email: string, method: string, clientId?: string, profileArn?: string): string;
3
+ export declare class AccountManager {
4
+ private accounts;
5
+ private cursor;
6
+ private strategy;
7
+ private lastToastTime;
8
+ private lastUsageToastTime;
9
+ constructor(accounts: ManagedAccount[], strategy?: AccountSelectionStrategy);
10
+ static loadFromDisk(strategy?: AccountSelectionStrategy): Promise<AccountManager>;
11
+ getAccountCount(): number;
12
+ getAccounts(): ManagedAccount[];
13
+ shouldShowToast(debounce?: number): boolean;
14
+ shouldShowUsageToast(debounce?: number): boolean;
15
+ getMinWaitTime(): number;
16
+ getCurrentOrNext(): ManagedAccount | null;
17
+ updateUsage(id: string, meta: {
18
+ usedCount: number;
19
+ limitCount: number;
20
+ email?: string;
21
+ }): void;
22
+ addAccount(a: ManagedAccount): void;
23
+ removeAccount(a: ManagedAccount): void;
24
+ updateFromAuth(a: ManagedAccount, auth: KiroAuthDetails): void;
25
+ markRateLimited(a: ManagedAccount, ms: number): void;
26
+ markUnhealthy(a: ManagedAccount, reason: string, recovery?: number): void;
27
+ saveToDisk(): Promise<void>;
28
+ toAuthDetails(a: ManagedAccount): KiroAuthDetails;
29
+ }
@@ -0,0 +1,235 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { decodeRefreshToken, encodeRefreshToken } from '../kiro/auth';
3
+ import { isPermanentError } from './health';
4
+ import * as logger from './logger';
5
+ import { kiroDb } from './storage/sqlite';
6
+ import { writeToKiroCli } from './sync/kiro-cli';
7
+ export function createDeterministicAccountId(email, method, clientId, profileArn) {
8
+ return createHash('sha256')
9
+ .update(`${email}:${method}:${clientId || ''}:${profileArn || ''}`)
10
+ .digest('hex');
11
+ }
12
+ export class AccountManager {
13
+ accounts;
14
+ cursor;
15
+ strategy;
16
+ lastToastTime = 0;
17
+ lastUsageToastTime = 0;
18
+ constructor(accounts, strategy = 'sticky') {
19
+ this.accounts = accounts;
20
+ this.cursor = 0;
21
+ this.strategy = strategy;
22
+ }
23
+ static async loadFromDisk(strategy) {
24
+ const rows = kiroDb.getAccounts();
25
+ const accounts = rows.map((r) => ({
26
+ id: r.id,
27
+ email: r.email,
28
+ authMethod: r.auth_method,
29
+ region: r.region,
30
+ clientId: r.client_id,
31
+ clientSecret: r.client_secret,
32
+ profileArn: r.profile_arn,
33
+ refreshToken: r.refresh_token,
34
+ accessToken: r.access_token,
35
+ expiresAt: r.expires_at,
36
+ rateLimitResetTime: r.rate_limit_reset,
37
+ isHealthy: r.is_healthy === 1,
38
+ unhealthyReason: r.unhealthy_reason,
39
+ recoveryTime: r.recovery_time,
40
+ failCount: r.fail_count || 0,
41
+ lastUsed: r.last_used,
42
+ usedCount: r.used_count,
43
+ limitCount: r.limit_count
44
+ }));
45
+ return new AccountManager(accounts, strategy || 'sticky');
46
+ }
47
+ getAccountCount() {
48
+ return this.accounts.length;
49
+ }
50
+ getAccounts() {
51
+ return [...this.accounts];
52
+ }
53
+ shouldShowToast(debounce = 10000) {
54
+ if (Date.now() - this.lastToastTime < debounce)
55
+ return false;
56
+ this.lastToastTime = Date.now();
57
+ return true;
58
+ }
59
+ shouldShowUsageToast(debounce = 10000) {
60
+ if (Date.now() - this.lastUsageToastTime < debounce)
61
+ return false;
62
+ this.lastUsageToastTime = Date.now();
63
+ return true;
64
+ }
65
+ getMinWaitTime() {
66
+ const now = Date.now();
67
+ const waits = this.accounts.map((a) => (a.rateLimitResetTime || 0) - now).filter((t) => t > 0);
68
+ return waits.length > 0 ? Math.min(...waits) : 0;
69
+ }
70
+ getCurrentOrNext() {
71
+ const now = Date.now();
72
+ const available = this.accounts.filter((a) => {
73
+ if (!a.isHealthy) {
74
+ if (isPermanentError(a.unhealthyReason)) {
75
+ return false;
76
+ }
77
+ if (a.failCount < 10 && a.recoveryTime && now >= a.recoveryTime) {
78
+ a.isHealthy = true;
79
+ delete a.unhealthyReason;
80
+ delete a.recoveryTime;
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+ return !(a.rateLimitResetTime && now < a.rateLimitResetTime);
86
+ });
87
+ let selected;
88
+ if (available.length > 0) {
89
+ if (this.strategy === 'sticky') {
90
+ selected = available.find((_, i) => i === this.cursor) || available[0];
91
+ }
92
+ else if (this.strategy === 'round-robin') {
93
+ selected = available[this.cursor % available.length];
94
+ this.cursor = (this.cursor + 1) % available.length;
95
+ }
96
+ else if (this.strategy === 'lowest-usage') {
97
+ selected = [...available].sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
98
+ }
99
+ }
100
+ if (!selected) {
101
+ const fallback = this.accounts
102
+ .filter((a) => !a.isHealthy && a.failCount < 10 && !isPermanentError(a.unhealthyReason))
103
+ .sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
104
+ if (fallback) {
105
+ fallback.isHealthy = true;
106
+ delete fallback.unhealthyReason;
107
+ delete fallback.recoveryTime;
108
+ selected = fallback;
109
+ }
110
+ }
111
+ if (selected) {
112
+ selected.lastUsed = now;
113
+ selected.usedCount = (selected.usedCount || 0) + 1;
114
+ this.cursor = this.accounts.indexOf(selected);
115
+ return selected;
116
+ }
117
+ return null;
118
+ }
119
+ updateUsage(id, meta) {
120
+ const a = this.accounts.find((x) => x.id === id);
121
+ if (a) {
122
+ a.usedCount = meta.usedCount;
123
+ a.limitCount = meta.limitCount;
124
+ if (meta.email)
125
+ a.email = meta.email;
126
+ if (!isPermanentError(a.unhealthyReason)) {
127
+ a.failCount = 0;
128
+ a.isHealthy = true;
129
+ delete a.unhealthyReason;
130
+ delete a.recoveryTime;
131
+ }
132
+ kiroDb.upsertAccount(a).catch(() => { });
133
+ }
134
+ }
135
+ addAccount(a) {
136
+ const i = this.accounts.findIndex((x) => x.id === a.id);
137
+ if (i === -1)
138
+ this.accounts.push(a);
139
+ else
140
+ this.accounts[i] = a;
141
+ kiroDb.upsertAccount(a).catch(() => { });
142
+ }
143
+ removeAccount(a) {
144
+ const removedIndex = this.accounts.findIndex((x) => x.id === a.id);
145
+ if (removedIndex === -1)
146
+ return;
147
+ this.accounts = this.accounts.filter((x) => x.id !== a.id);
148
+ kiroDb.deleteAccount(a.id).catch(() => { });
149
+ if (this.accounts.length === 0)
150
+ this.cursor = 0;
151
+ else if (this.cursor >= this.accounts.length)
152
+ this.cursor = this.accounts.length - 1;
153
+ else if (removedIndex <= this.cursor && this.cursor > 0)
154
+ this.cursor--;
155
+ }
156
+ updateFromAuth(a, auth) {
157
+ const acc = this.accounts.find((x) => x.id === a.id);
158
+ if (acc) {
159
+ acc.accessToken = auth.access;
160
+ acc.expiresAt = auth.expires;
161
+ acc.lastUsed = Date.now();
162
+ if (auth.email)
163
+ acc.email = auth.email;
164
+ const p = decodeRefreshToken(auth.refresh);
165
+ acc.refreshToken = p.refreshToken;
166
+ if (p.profileArn)
167
+ acc.profileArn = p.profileArn;
168
+ if (p.clientId)
169
+ acc.clientId = p.clientId;
170
+ acc.failCount = 0;
171
+ acc.isHealthy = true;
172
+ delete acc.unhealthyReason;
173
+ delete acc.recoveryTime;
174
+ kiroDb.upsertAccount(acc).catch(() => { });
175
+ writeToKiroCli(acc).catch(() => { });
176
+ }
177
+ }
178
+ markRateLimited(a, ms) {
179
+ const acc = this.accounts.find((x) => x.id === a.id);
180
+ if (acc) {
181
+ acc.rateLimitResetTime = Date.now() + ms;
182
+ kiroDb.upsertAccount(acc).catch(() => { });
183
+ }
184
+ }
185
+ markUnhealthy(a, reason, recovery) {
186
+ const acc = this.accounts.find((x) => x.id === a.id);
187
+ if (!acc)
188
+ return;
189
+ const isPermanent = isPermanentError(reason);
190
+ if (isPermanent) {
191
+ logger.warn('Account marked as permanently unhealthy', {
192
+ email: acc.email,
193
+ reason,
194
+ accountId: acc.id
195
+ });
196
+ acc.failCount = 10;
197
+ acc.isHealthy = false;
198
+ acc.unhealthyReason = reason;
199
+ delete acc.recoveryTime;
200
+ }
201
+ else {
202
+ acc.failCount = (acc.failCount || 0) + 1;
203
+ acc.unhealthyReason = reason;
204
+ acc.lastUsed = Date.now();
205
+ if (acc.failCount >= 10) {
206
+ acc.isHealthy = false;
207
+ acc.recoveryTime = recovery || Date.now() + 3600000;
208
+ }
209
+ }
210
+ kiroDb.upsertAccount(acc).catch(() => { });
211
+ }
212
+ async saveToDisk() {
213
+ await kiroDb.batchUpsertAccounts(this.accounts);
214
+ }
215
+ toAuthDetails(a) {
216
+ const p = {
217
+ refreshToken: a.refreshToken,
218
+ profileArn: a.profileArn,
219
+ clientId: a.clientId,
220
+ clientSecret: a.clientSecret,
221
+ authMethod: a.authMethod
222
+ };
223
+ return {
224
+ refresh: encodeRefreshToken(p),
225
+ access: a.accessToken,
226
+ expires: a.expiresAt,
227
+ authMethod: a.authMethod,
228
+ region: a.region,
229
+ profileArn: a.profileArn,
230
+ clientId: a.clientId,
231
+ clientSecret: a.clientSecret,
232
+ email: a.email
233
+ };
234
+ }
235
+ }
@@ -0,0 +1,3 @@
1
+ export declare function getIDCAuthHtml(verificationUrl: string, userCode: string, statusUrl: string): string;
2
+ export declare function getSuccessHtml(): string;
3
+ export declare function getErrorHtml(message: string): string;