@zhafron/opencode-kiro-auth 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +85 -0
  2. package/dist/constants.d.ts +26 -0
  3. package/dist/constants.js +60 -0
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.js +1 -0
  6. package/dist/kiro/auth.d.ts +5 -0
  7. package/dist/kiro/auth.js +24 -0
  8. package/dist/kiro/oauth-idc.d.ts +24 -0
  9. package/dist/kiro/oauth-idc.js +132 -0
  10. package/dist/kiro/oauth-social.d.ts +17 -0
  11. package/dist/kiro/oauth-social.js +51 -0
  12. package/dist/plugin/accounts.d.ts +28 -0
  13. package/dist/plugin/accounts.js +156 -0
  14. package/dist/plugin/auth-page.d.ts +3 -0
  15. package/dist/plugin/auth-page.js +568 -0
  16. package/dist/plugin/cli.d.ts +6 -0
  17. package/dist/plugin/cli.js +98 -0
  18. package/dist/plugin/config/index.d.ts +3 -0
  19. package/dist/plugin/config/index.js +2 -0
  20. package/dist/plugin/config/loader.d.ts +6 -0
  21. package/dist/plugin/config/loader.js +125 -0
  22. package/dist/plugin/config/schema.d.ts +44 -0
  23. package/dist/plugin/config/schema.js +28 -0
  24. package/dist/plugin/debug.d.ts +2 -0
  25. package/dist/plugin/debug.js +9 -0
  26. package/dist/plugin/errors.d.ts +17 -0
  27. package/dist/plugin/errors.js +34 -0
  28. package/dist/plugin/logger.d.ts +4 -0
  29. package/dist/plugin/logger.js +37 -0
  30. package/dist/plugin/models.d.ts +3 -0
  31. package/dist/plugin/models.js +14 -0
  32. package/dist/plugin/oauth-parser.d.ts +5 -0
  33. package/dist/plugin/oauth-parser.js +23 -0
  34. package/dist/plugin/quota.d.ts +15 -0
  35. package/dist/plugin/quota.js +68 -0
  36. package/dist/plugin/recovery.d.ts +19 -0
  37. package/dist/plugin/recovery.js +302 -0
  38. package/dist/plugin/refresh-queue.d.ts +14 -0
  39. package/dist/plugin/refresh-queue.js +69 -0
  40. package/dist/plugin/request.d.ts +4 -0
  41. package/dist/plugin/request.js +240 -0
  42. package/dist/plugin/response.d.ts +6 -0
  43. package/dist/plugin/response.js +246 -0
  44. package/dist/plugin/server.d.ts +24 -0
  45. package/dist/plugin/server.js +96 -0
  46. package/dist/plugin/storage.d.ts +7 -0
  47. package/dist/plugin/storage.js +75 -0
  48. package/dist/plugin/streaming.d.ts +3 -0
  49. package/dist/plugin/streaming.js +503 -0
  50. package/dist/plugin/token.d.ts +2 -0
  51. package/dist/plugin/token.js +56 -0
  52. package/dist/plugin/types.d.ts +148 -0
  53. package/dist/plugin/types.js +0 -0
  54. package/dist/plugin/usage.d.ts +3 -0
  55. package/dist/plugin/usage.js +36 -0
  56. package/dist/plugin.d.ts +32 -0
  57. package/dist/plugin.js +222 -0
  58. package/dist/src/constants.d.ts +22 -0
  59. package/dist/src/constants.js +35 -0
  60. package/dist/src/kiro/auth.d.ts +5 -0
  61. package/dist/src/kiro/auth.js +69 -0
  62. package/dist/src/kiro/oauth-idc.d.ts +22 -0
  63. package/dist/src/kiro/oauth-idc.js +99 -0
  64. package/dist/src/kiro/oauth-social.d.ts +17 -0
  65. package/dist/src/kiro/oauth-social.js +69 -0
  66. package/dist/src/plugin/accounts.d.ts +23 -0
  67. package/dist/src/plugin/accounts.js +265 -0
  68. package/dist/src/plugin/cli.d.ts +6 -0
  69. package/dist/src/plugin/cli.js +98 -0
  70. package/dist/src/plugin/config/index.d.ts +3 -0
  71. package/dist/src/plugin/config/index.js +2 -0
  72. package/dist/src/plugin/config/loader.d.ts +7 -0
  73. package/dist/src/plugin/config/loader.js +143 -0
  74. package/dist/src/plugin/config/schema.d.ts +68 -0
  75. package/dist/src/plugin/config/schema.js +44 -0
  76. package/dist/src/plugin/debug.d.ts +2 -0
  77. package/dist/src/plugin/debug.js +9 -0
  78. package/dist/src/plugin/errors.d.ts +17 -0
  79. package/dist/src/plugin/errors.js +34 -0
  80. package/dist/src/plugin/logger.d.ts +4 -0
  81. package/dist/src/plugin/logger.js +17 -0
  82. package/dist/src/plugin/models.d.ts +3 -0
  83. package/dist/src/plugin/models.js +14 -0
  84. package/dist/src/plugin/oauth-parser.d.ts +5 -0
  85. package/dist/src/plugin/oauth-parser.js +23 -0
  86. package/dist/src/plugin/quota.d.ts +25 -0
  87. package/dist/src/plugin/quota.js +175 -0
  88. package/dist/src/plugin/recovery.d.ts +19 -0
  89. package/dist/src/plugin/recovery.js +302 -0
  90. package/dist/src/plugin/refresh-queue.d.ts +14 -0
  91. package/dist/src/plugin/refresh-queue.js +69 -0
  92. package/dist/src/plugin/request.d.ts +35 -0
  93. package/dist/src/plugin/request.js +411 -0
  94. package/dist/src/plugin/response.d.ts +6 -0
  95. package/dist/src/plugin/response.js +246 -0
  96. package/dist/src/plugin/server.d.ts +10 -0
  97. package/dist/src/plugin/server.js +203 -0
  98. package/dist/src/plugin/storage.d.ts +5 -0
  99. package/dist/src/plugin/storage.js +106 -0
  100. package/dist/src/plugin/streaming.d.ts +12 -0
  101. package/dist/src/plugin/streaming.js +444 -0
  102. package/dist/src/plugin/token.d.ts +8 -0
  103. package/dist/src/plugin/token.js +130 -0
  104. package/dist/src/plugin/types.d.ts +144 -0
  105. package/dist/src/plugin/types.js +0 -0
  106. package/dist/src/plugin/usage.d.ts +28 -0
  107. package/dist/src/plugin/usage.js +159 -0
  108. package/dist/src/plugin.d.ts +2 -0
  109. package/dist/src/plugin.js +341 -0
  110. package/package.json +57 -0
@@ -0,0 +1,144 @@
1
+ export type KiroAuthMethod = 'social' | 'idc';
2
+ export type KiroRegion = 'us-east-1' | 'us-west-2';
3
+ export interface KiroAuthDetails {
4
+ refresh: string;
5
+ access: string;
6
+ expires: number;
7
+ authMethod: KiroAuthMethod;
8
+ region: KiroRegion;
9
+ profileArn?: string;
10
+ clientId?: string;
11
+ clientSecret?: string;
12
+ email?: string;
13
+ }
14
+ export interface RefreshParts {
15
+ refreshToken: string;
16
+ profileArn?: string;
17
+ clientId?: string;
18
+ clientSecret?: string;
19
+ authMethod?: KiroAuthMethod;
20
+ }
21
+ export interface ManagedAccount {
22
+ id: string;
23
+ email: string;
24
+ authMethod: KiroAuthMethod;
25
+ region: KiroRegion;
26
+ profileArn?: string;
27
+ clientId?: string;
28
+ clientSecret?: string;
29
+ refreshToken: string;
30
+ accessToken: string;
31
+ expiresAt: number;
32
+ rateLimitResetTime: number;
33
+ isHealthy: boolean;
34
+ unhealthyReason?: string;
35
+ recoveryTime?: number;
36
+ usedCount?: number;
37
+ limitCount?: number;
38
+ lastUsed?: number;
39
+ }
40
+ export interface AccountMetadata {
41
+ id: string;
42
+ email: string;
43
+ authMethod: KiroAuthMethod;
44
+ region: KiroRegion;
45
+ profileArn?: string;
46
+ clientId?: string;
47
+ clientSecret?: string;
48
+ refreshToken: string;
49
+ accessToken: string;
50
+ expiresAt: number;
51
+ rateLimitResetTime: number;
52
+ isHealthy: boolean;
53
+ unhealthyReason?: string;
54
+ recoveryTime?: number;
55
+ usedCount?: number;
56
+ limitCount?: number;
57
+ }
58
+ export interface AccountStorage {
59
+ version: 1;
60
+ accounts: AccountMetadata[];
61
+ activeIndex: number;
62
+ }
63
+ export interface CodeWhispererMessage {
64
+ userInputMessage?: {
65
+ content: string;
66
+ modelId: string;
67
+ origin: string;
68
+ images?: Array<{
69
+ format: string;
70
+ source: {
71
+ bytes: string;
72
+ };
73
+ }>;
74
+ userInputMessageContext?: {
75
+ toolResults?: Array<{
76
+ toolUseId: string;
77
+ content: Array<{
78
+ text?: string;
79
+ image?: {
80
+ format: string;
81
+ source: {
82
+ bytes: string;
83
+ };
84
+ };
85
+ }>;
86
+ status?: string;
87
+ }>;
88
+ tools?: Array<{
89
+ toolSpecification: {
90
+ name: string;
91
+ description: string;
92
+ inputSchema: {
93
+ json: Record<string, unknown>;
94
+ };
95
+ };
96
+ }>;
97
+ };
98
+ };
99
+ assistantResponseMessage?: {
100
+ content: string;
101
+ };
102
+ }
103
+ export interface CodeWhispererRequest {
104
+ conversationState: {
105
+ chatTriggerType: string;
106
+ conversationId: string;
107
+ history: CodeWhispererMessage[];
108
+ currentMessage: CodeWhispererMessage;
109
+ };
110
+ profileArn?: string;
111
+ }
112
+ export interface ToolCall {
113
+ toolUseId: string;
114
+ name: string;
115
+ input: string | Record<string, unknown>;
116
+ }
117
+ export interface ParsedResponse {
118
+ content: string;
119
+ toolCalls: ToolCall[];
120
+ stopReason?: string;
121
+ inputTokens?: number;
122
+ outputTokens?: number;
123
+ }
124
+ export interface UsageLimits {
125
+ usedCount: number;
126
+ limitCount: number;
127
+ contextUsagePercentage?: number;
128
+ }
129
+ export interface PreparedRequest {
130
+ url: string;
131
+ init: RequestInit;
132
+ streaming: boolean;
133
+ effectiveModel: string;
134
+ conversationId: string;
135
+ }
136
+ export type AccountSelectionStrategy = 'sticky' | 'round-robin';
137
+ export interface StreamEvent {
138
+ type: string;
139
+ message?: any;
140
+ content_block?: any;
141
+ delta?: any;
142
+ index?: number;
143
+ usage?: any;
144
+ }
File without changes
@@ -0,0 +1,28 @@
1
+ import { KiroAuthDetails, UsageLimits, ParsedResponse } from './types';
2
+ export declare function fetchUsageLimits(auth: KiroAuthDetails): Promise<UsageLimits>;
3
+ export declare function calculateRecoveryTime(): number;
4
+ export declare function isQuotaExhausted(usage: UsageLimits): boolean;
5
+ export declare function formatUsageDisplay(usage: UsageLimits): string;
6
+ export declare function calculateUsagePercentage(usage: UsageLimits): number;
7
+ export declare function getRemainingCount(usage: UsageLimits): number;
8
+ export declare function extractUsageFromResponse(response: ParsedResponse): {
9
+ inputTokens: number;
10
+ outputTokens: number;
11
+ } | null;
12
+ export declare function shouldRefreshUsage(lastFetchTime: number | undefined, intervalMs?: number): boolean;
13
+ export declare function isUsageWarningThreshold(usage: UsageLimits, thresholdPercent?: number): boolean;
14
+ export declare function getUsageStatus(usage: UsageLimits): 'healthy' | 'warning' | 'exhausted';
15
+ export declare function formatRecoveryTime(recoveryTimeMs: number): string;
16
+ export declare function getTimeUntilRecovery(recoveryTimeMs: number): number;
17
+ export declare function formatTimeUntilRecovery(recoveryTimeMs: number): string;
18
+ export interface UsageMetrics {
19
+ usage: UsageLimits;
20
+ status: 'healthy' | 'warning' | 'exhausted';
21
+ percentage: number;
22
+ remaining: number;
23
+ recoveryTime?: number;
24
+ timeUntilRecovery?: number;
25
+ formattedDisplay: string;
26
+ }
27
+ export declare function buildUsageMetrics(usage: UsageLimits, recoveryTime?: number): UsageMetrics;
28
+ export declare function fetchAndBuildUsageMetrics(auth: KiroAuthDetails): Promise<UsageMetrics>;
@@ -0,0 +1,159 @@
1
+ const USAGE_LIMITS_ENDPOINT = 'https://q.{{region}}.amazonaws.com/getUsageLimits';
2
+ const RESOURCE_TYPE = 'AGENTIC_REQUEST';
3
+ const ORIGIN = 'AI_EDITOR';
4
+ export async function fetchUsageLimits(auth) {
5
+ const url = buildUsageLimitsUrl(auth);
6
+ const headers = buildRequestHeaders(auth);
7
+ try {
8
+ const response = await fetch(url, {
9
+ method: 'GET',
10
+ headers,
11
+ });
12
+ if (!response.ok) {
13
+ throw new Error(`Usage limits request failed: ${response.status} ${response.statusText}`);
14
+ }
15
+ const data = await response.json();
16
+ return parseUsageLimitsResponse(data);
17
+ }
18
+ catch (error) {
19
+ if (error instanceof Error) {
20
+ throw new Error(`Failed to fetch usage limits: ${error.message}`);
21
+ }
22
+ throw new Error('Failed to fetch usage limits: Unknown error');
23
+ }
24
+ }
25
+ function buildUsageLimitsUrl(auth) {
26
+ const baseUrl = USAGE_LIMITS_ENDPOINT.replace('{{region}}', auth.region);
27
+ const params = new URLSearchParams({
28
+ isEmailRequired: 'true',
29
+ origin: ORIGIN,
30
+ resourceType: RESOURCE_TYPE,
31
+ });
32
+ if (auth.authMethod === 'social' && auth.profileArn) {
33
+ params.append('profileArn', auth.profileArn);
34
+ }
35
+ return `${baseUrl}?${params.toString()}`;
36
+ }
37
+ function buildRequestHeaders(auth) {
38
+ return {
39
+ 'Authorization': `Bearer ${auth.access}`,
40
+ 'Content-Type': 'application/json',
41
+ 'x-amzn-kiro-agent-mode': 'vibe',
42
+ 'amz-sdk-request': 'attempt=1; max=1',
43
+ };
44
+ }
45
+ function parseUsageLimitsResponse(data) {
46
+ const usedCount = typeof data.usedCount === 'number' ? data.usedCount : 0;
47
+ const limitCount = typeof data.limitCount === 'number' ? data.limitCount : 0;
48
+ const contextUsagePercentage = typeof data.contextUsagePercentage === 'number'
49
+ ? data.contextUsagePercentage
50
+ : undefined;
51
+ return {
52
+ usedCount,
53
+ limitCount,
54
+ contextUsagePercentage,
55
+ };
56
+ }
57
+ export function calculateRecoveryTime() {
58
+ const now = new Date();
59
+ const nextMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0));
60
+ return nextMonth.getTime();
61
+ }
62
+ export function isQuotaExhausted(usage) {
63
+ return usage.usedCount >= usage.limitCount;
64
+ }
65
+ export function formatUsageDisplay(usage) {
66
+ const percentage = usage.limitCount > 0
67
+ ? Math.round((usage.usedCount / usage.limitCount) * 100)
68
+ : 0;
69
+ return `${usage.usedCount}/${usage.limitCount} (${percentage}%)`;
70
+ }
71
+ export function calculateUsagePercentage(usage) {
72
+ if (usage.limitCount <= 0) {
73
+ return 0;
74
+ }
75
+ return Math.round((usage.usedCount / usage.limitCount) * 100);
76
+ }
77
+ export function getRemainingCount(usage) {
78
+ const remaining = usage.limitCount - usage.usedCount;
79
+ return Math.max(0, remaining);
80
+ }
81
+ export function extractUsageFromResponse(response) {
82
+ const inputTokens = response.inputTokens;
83
+ const outputTokens = response.outputTokens;
84
+ if (typeof inputTokens === 'number' && typeof outputTokens === 'number') {
85
+ return {
86
+ inputTokens,
87
+ outputTokens,
88
+ };
89
+ }
90
+ return null;
91
+ }
92
+ export function shouldRefreshUsage(lastFetchTime, intervalMs = 300000) {
93
+ if (!lastFetchTime) {
94
+ return true;
95
+ }
96
+ return Date.now() - lastFetchTime >= intervalMs;
97
+ }
98
+ export function isUsageWarningThreshold(usage, thresholdPercent = 80) {
99
+ const percentage = calculateUsagePercentage(usage);
100
+ return percentage >= thresholdPercent && percentage < 100;
101
+ }
102
+ export function getUsageStatus(usage) {
103
+ if (isQuotaExhausted(usage)) {
104
+ return 'exhausted';
105
+ }
106
+ if (isUsageWarningThreshold(usage)) {
107
+ return 'warning';
108
+ }
109
+ return 'healthy';
110
+ }
111
+ export function formatRecoveryTime(recoveryTimeMs) {
112
+ const date = new Date(recoveryTimeMs);
113
+ return date.toISOString();
114
+ }
115
+ export function getTimeUntilRecovery(recoveryTimeMs) {
116
+ const now = Date.now();
117
+ const timeUntil = recoveryTimeMs - now;
118
+ return Math.max(0, timeUntil);
119
+ }
120
+ export function formatTimeUntilRecovery(recoveryTimeMs) {
121
+ const ms = getTimeUntilRecovery(recoveryTimeMs);
122
+ const seconds = Math.floor(ms / 1000);
123
+ const minutes = Math.floor(seconds / 60);
124
+ const hours = Math.floor(minutes / 60);
125
+ const days = Math.floor(hours / 24);
126
+ if (days > 0) {
127
+ return `${days}d ${hours % 24}h`;
128
+ }
129
+ if (hours > 0) {
130
+ return `${hours}h ${minutes % 60}m`;
131
+ }
132
+ if (minutes > 0) {
133
+ return `${minutes}m`;
134
+ }
135
+ return `${seconds}s`;
136
+ }
137
+ export function buildUsageMetrics(usage, recoveryTime) {
138
+ const status = getUsageStatus(usage);
139
+ const percentage = calculateUsagePercentage(usage);
140
+ const remaining = getRemainingCount(usage);
141
+ const formattedDisplay = formatUsageDisplay(usage);
142
+ const metrics = {
143
+ usage,
144
+ status,
145
+ percentage,
146
+ remaining,
147
+ formattedDisplay,
148
+ };
149
+ if (recoveryTime) {
150
+ metrics.recoveryTime = recoveryTime;
151
+ metrics.timeUntilRecovery = getTimeUntilRecovery(recoveryTime);
152
+ }
153
+ return metrics;
154
+ }
155
+ export async function fetchAndBuildUsageMetrics(auth) {
156
+ const usage = await fetchUsageLimits(auth);
157
+ const recoveryTime = isQuotaExhausted(usage) ? calculateRecoveryTime() : undefined;
158
+ return buildUsageMetrics(usage, recoveryTime);
159
+ }
@@ -0,0 +1,2 @@
1
+ export declare const createKiroPlugin: (providerId: string) => ({ client, directory }: any) => Promise<any>;
2
+ export declare const KiroOAuthPlugin: ({ client, directory }: any) => Promise<any>;
@@ -0,0 +1,341 @@
1
+ import { loadConfig } from './plugin/config';
2
+ import { AccountManager, generateAccountId } from './plugin/accounts';
3
+ import { createProactiveRefreshQueue } from './plugin/refresh-queue';
4
+ import { createSessionRecoveryHook } from './plugin/recovery';
5
+ import { accessTokenExpired, encodeRefreshToken } from './kiro/auth';
6
+ import { refreshAccessToken } from './plugin/token';
7
+ import { transformToCodeWhisperer } from './plugin/request';
8
+ import { parseEventStream } from './plugin/response';
9
+ import { transformKiroStream } from './plugin/streaming';
10
+ import { fetchUsageLimits, calculateRecoveryTime } from './plugin/usage';
11
+ import { updateAccountQuota } from './plugin/quota';
12
+ import { authorizeKiroSocial, exchangeKiroSocial } from './kiro/oauth-social';
13
+ import { authorizeKiroIDC, exchangeKiroIDC } from './kiro/oauth-idc';
14
+ import { parseOAuthCallbackInput } from './plugin/oauth-parser';
15
+ import { KiroTokenRefreshError } from './plugin/errors';
16
+ import { KIRO_CONSTANTS } from './constants';
17
+ const KIRO_PROVIDER_ID = 'kiro';
18
+ const KIRO_API_PATTERN = /q\.(us-east-1|us-west-2)\.amazonaws\.com/;
19
+ function sleep(ms) {
20
+ return new Promise(resolve => setTimeout(resolve, ms));
21
+ }
22
+ function isNetworkError(error) {
23
+ if (error instanceof Error) {
24
+ const message = error.message.toLowerCase();
25
+ return message.includes('econnreset') ||
26
+ message.includes('etimedout') ||
27
+ message.includes('enotfound') ||
28
+ message.includes('network') ||
29
+ message.includes('fetch failed');
30
+ }
31
+ return false;
32
+ }
33
+ function extractModelFromUrl(url) {
34
+ const match = url.match(/models\/([^/:]+)/);
35
+ return match?.[1] || null;
36
+ }
37
+ export const createKiroPlugin = (providerId) => async ({ client, directory }) => {
38
+ const config = loadConfig(directory);
39
+ const sessionRecovery = createSessionRecoveryHook(config.session_recovery, config.auto_resume);
40
+ return {
41
+ event: async (event) => {
42
+ if (event.type === 'session.error') {
43
+ await sessionRecovery.handleSessionError(event.error, event.sessionId);
44
+ }
45
+ },
46
+ auth: {
47
+ provider: providerId,
48
+ loader: async (getAuth, provider) => {
49
+ const auth = await getAuth();
50
+ const accountManager = await AccountManager.loadFromDisk(config.account_selection_strategy);
51
+ if (accountManager.getAccountCount() === 0) {
52
+ return {
53
+ apiKey: '',
54
+ async fetch(input, init) {
55
+ throw new Error('No Kiro accounts configured. Please run: opencode auth login');
56
+ }
57
+ };
58
+ }
59
+ const refreshQueue = createProactiveRefreshQueue({
60
+ enabled: config.proactive_token_refresh,
61
+ checkIntervalSeconds: config.token_refresh_interval_seconds,
62
+ bufferSeconds: config.token_refresh_buffer_seconds,
63
+ });
64
+ refreshQueue.setAccountManager(accountManager);
65
+ refreshQueue.start();
66
+ return {
67
+ apiKey: '',
68
+ async fetch(input, init) {
69
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
70
+ if (!KIRO_API_PATTERN.test(url)) {
71
+ return fetch(input, init);
72
+ }
73
+ const body = init?.body ? JSON.parse(init.body) : {};
74
+ const model = extractModelFromUrl(url) || body.model || 'claude-opus-4-5';
75
+ const providerOptions = body.providerOptions || {};
76
+ const thinkingConfig = providerOptions.thinkingConfig;
77
+ const thinkingEnabled = !!thinkingConfig;
78
+ const thinkingBudget = thinkingConfig?.thinkingBudget || config.thinking_budget_tokens;
79
+ let retryCount = 0;
80
+ const maxRetries = config.rate_limit_max_retries;
81
+ while (retryCount <= maxRetries) {
82
+ const account = accountManager.getCurrentOrNext();
83
+ if (!account) {
84
+ throw new Error('No available Kiro accounts. Please run: opencode auth login');
85
+ }
86
+ const authDetails = accountManager.toAuthDetails(account);
87
+ if (accessTokenExpired(authDetails)) {
88
+ try {
89
+ const refreshed = await refreshAccessToken(authDetails);
90
+ accountManager.updateFromAuth(account, refreshed);
91
+ await accountManager.saveToDisk();
92
+ }
93
+ catch (error) {
94
+ if (error instanceof KiroTokenRefreshError && error.code === 'invalid_grant') {
95
+ accountManager.removeAccount(account);
96
+ await accountManager.saveToDisk();
97
+ continue;
98
+ }
99
+ throw error;
100
+ }
101
+ }
102
+ const prepared = transformToCodeWhisperer(url, init?.body, model, authDetails, thinkingEnabled, thinkingBudget);
103
+ try {
104
+ const response = await fetch(prepared.url, prepared.init);
105
+ if (!response.ok) {
106
+ const status = response.status;
107
+ if (status === 401 && retryCount === 0) {
108
+ const refreshed = await refreshAccessToken(authDetails);
109
+ accountManager.updateFromAuth(account, refreshed);
110
+ await accountManager.saveToDisk();
111
+ retryCount++;
112
+ continue;
113
+ }
114
+ if (status === 402) {
115
+ const recoveryTime = calculateRecoveryTime();
116
+ accountManager.markUnhealthy(account, 'Quota exhausted', recoveryTime);
117
+ await accountManager.saveToDisk();
118
+ retryCount++;
119
+ continue;
120
+ }
121
+ if (status === 403) {
122
+ accountManager.markUnhealthy(account, 'Forbidden');
123
+ await accountManager.saveToDisk();
124
+ retryCount++;
125
+ continue;
126
+ }
127
+ if (status === 429) {
128
+ const retryAfter = parseInt(response.headers.get('retry-after') || '60') * 1000;
129
+ accountManager.markRateLimited(account, retryAfter);
130
+ await accountManager.saveToDisk();
131
+ await sleep(config.rate_limit_retry_delay_ms);
132
+ retryCount++;
133
+ continue;
134
+ }
135
+ throw new Error(`Kiro API error: ${status}`);
136
+ }
137
+ if (config.usage_tracking_enabled) {
138
+ try {
139
+ const usage = await fetchUsageLimits(authDetails);
140
+ updateAccountQuota(account, usage);
141
+ await accountManager.saveToDisk();
142
+ }
143
+ catch (error) {
144
+ if (config.debug) {
145
+ console.error('Failed to fetch usage:', error);
146
+ }
147
+ }
148
+ }
149
+ if (prepared.streaming) {
150
+ const stream = transformKiroStream(response, model, prepared.conversationId);
151
+ return new Response(new ReadableStream({
152
+ async start(controller) {
153
+ try {
154
+ for await (const event of stream) {
155
+ controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(event)}\n\n`));
156
+ }
157
+ controller.close();
158
+ }
159
+ catch (error) {
160
+ controller.error(error);
161
+ }
162
+ }
163
+ }), {
164
+ headers: {
165
+ 'Content-Type': 'text/event-stream',
166
+ 'Cache-Control': 'no-cache',
167
+ 'Connection': 'keep-alive',
168
+ }
169
+ });
170
+ }
171
+ else {
172
+ const text = await response.text();
173
+ const parsed = parseEventStream(text);
174
+ const claudeResponse = {
175
+ id: prepared.conversationId,
176
+ type: 'message',
177
+ role: 'assistant',
178
+ model: model,
179
+ content: [
180
+ { type: 'text', text: parsed.content }
181
+ ],
182
+ usage: {
183
+ input_tokens: parsed.inputTokens || 0,
184
+ output_tokens: parsed.outputTokens || 0,
185
+ },
186
+ stop_reason: parsed.stopReason || 'end_turn',
187
+ };
188
+ if (parsed.toolCalls.length > 0) {
189
+ claudeResponse.content.push(...parsed.toolCalls.map(tc => ({
190
+ type: 'tool_use',
191
+ id: tc.toolUseId,
192
+ name: tc.name,
193
+ input: typeof tc.input === 'string' ? JSON.parse(tc.input) : tc.input,
194
+ })));
195
+ claudeResponse.stop_reason = 'tool_use';
196
+ }
197
+ return new Response(JSON.stringify(claudeResponse), {
198
+ headers: { 'Content-Type': 'application/json' }
199
+ });
200
+ }
201
+ }
202
+ catch (error) {
203
+ if (isNetworkError(error) && retryCount < maxRetries) {
204
+ await sleep(config.rate_limit_retry_delay_ms * Math.pow(2, retryCount));
205
+ retryCount++;
206
+ continue;
207
+ }
208
+ throw error;
209
+ }
210
+ }
211
+ throw new Error('Max retries exceeded');
212
+ }
213
+ };
214
+ },
215
+ methods: [
216
+ {
217
+ label: 'Google OAuth (Social)',
218
+ type: 'oauth',
219
+ authorize: async (inputs) => {
220
+ if (!inputs || !inputs.region) {
221
+ return {
222
+ url: '',
223
+ instructions: 'Use CLI: opencode auth login',
224
+ method: 'auto',
225
+ callback: async () => ({
226
+ type: 'failed',
227
+ error: 'TUI auth not supported. Use: opencode auth login'
228
+ }),
229
+ };
230
+ }
231
+ const region = inputs.region || KIRO_CONSTANTS.DEFAULT_REGION;
232
+ const auth = await authorizeKiroSocial(region);
233
+ return {
234
+ url: auth.url,
235
+ instructions: 'Visit the URL above, complete Google OAuth, then paste the full redirect URL or authorization code below.',
236
+ method: 'code',
237
+ callback: async (codeInput) => {
238
+ try {
239
+ const { code, state } = parseOAuthCallbackInput(codeInput);
240
+ const result = await exchangeKiroSocial(code, state);
241
+ const accountManager = await AccountManager.loadFromDisk();
242
+ const account = {
243
+ id: generateAccountId(),
244
+ email: result.email,
245
+ authMethod: 'social',
246
+ region: result.region,
247
+ profileArn: result.profileArn,
248
+ refreshToken: result.refreshToken,
249
+ accessToken: result.accessToken,
250
+ expiresAt: result.expiresAt,
251
+ rateLimitResetTime: 0,
252
+ isHealthy: true,
253
+ };
254
+ accountManager.addAccount(account);
255
+ await accountManager.saveToDisk();
256
+ return {
257
+ type: 'success',
258
+ refresh: encodeRefreshToken(result.refreshToken, result.profileArn, undefined, undefined, 'social'),
259
+ access: result.accessToken,
260
+ expires: result.expiresAt,
261
+ };
262
+ }
263
+ catch (error) {
264
+ return {
265
+ type: 'failed',
266
+ error: error instanceof Error ? error.message : 'Unknown error',
267
+ };
268
+ }
269
+ },
270
+ };
271
+ }
272
+ },
273
+ {
274
+ label: 'AWS Builder ID (IDC)',
275
+ type: 'oauth',
276
+ authorize: async (inputs) => {
277
+ if (!inputs || !inputs.region) {
278
+ return {
279
+ url: '',
280
+ instructions: 'Use CLI: opencode auth login',
281
+ method: 'auto',
282
+ callback: async () => ({
283
+ type: 'failed',
284
+ error: 'TUI auth not supported. Use: opencode auth login'
285
+ }),
286
+ };
287
+ }
288
+ const region = inputs.region || KIRO_CONSTANTS.DEFAULT_REGION;
289
+ const auth = await authorizeKiroIDC(region);
290
+ return {
291
+ url: auth.url,
292
+ instructions: 'Visit the URL above, complete AWS Builder ID authentication. The token exchange will happen automatically after you authorize.',
293
+ method: 'auto',
294
+ callback: async () => {
295
+ try {
296
+ const state = Buffer.from(JSON.stringify({
297
+ deviceCode: auth.deviceCode,
298
+ region: auth.region,
299
+ clientId: auth.clientId,
300
+ clientSecret: auth.clientSecret,
301
+ })).toString('base64url');
302
+ await new Promise(resolve => setTimeout(resolve, 5000));
303
+ const result = await exchangeKiroIDC(state);
304
+ const accountManager = await AccountManager.loadFromDisk();
305
+ const account = {
306
+ id: generateAccountId(),
307
+ email: result.email,
308
+ authMethod: 'idc',
309
+ region: result.region,
310
+ clientId: result.clientId,
311
+ clientSecret: result.clientSecret,
312
+ refreshToken: result.refreshToken,
313
+ accessToken: result.accessToken,
314
+ expiresAt: result.expiresAt,
315
+ rateLimitResetTime: 0,
316
+ isHealthy: true,
317
+ };
318
+ accountManager.addAccount(account);
319
+ await accountManager.saveToDisk();
320
+ return {
321
+ type: 'success',
322
+ refresh: encodeRefreshToken(result.refreshToken, undefined, result.clientId, result.clientSecret, 'idc'),
323
+ access: result.accessToken,
324
+ expires: result.expiresAt,
325
+ };
326
+ }
327
+ catch (error) {
328
+ return {
329
+ type: 'failed',
330
+ error: error instanceof Error ? error.message : 'Unknown error',
331
+ };
332
+ }
333
+ },
334
+ };
335
+ }
336
+ }
337
+ ]
338
+ }
339
+ };
340
+ };
341
+ export const KiroOAuthPlugin = createKiroPlugin(KIRO_PROVIDER_ID);