@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
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @zhafron/opencode-kiro-auth
2
+
3
+ OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to the latest Claude 3.5/4.5 models with substantial trial quotas.
4
+
5
+ ## Features
6
+
7
+ - AWS Builder ID (IDC) authentication with seamless device code flow.
8
+ - Intelligent multi-account rotation prioritized by lowest usage.
9
+ - Automated token refresh and rate limit handling with exponential backoff.
10
+ - Native thinking mode support via virtual model mappings.
11
+ - Decoupled storage for credentials and real-time usage metadata.
12
+
13
+ ## Installation
14
+
15
+ Add the plugin to your `opencode.json` or `opencode.jsonc`:
16
+
17
+ ```json
18
+ {
19
+ "plugin": ["@zhafron/opencode-kiro-auth"],
20
+ "provider": {
21
+ "kiro": {
22
+ "models": {
23
+ "claude-opus-4-5": {
24
+ "name": "Claude Opus 4.5",
25
+ "limit": { "context": 200000, "output": 64000 },
26
+ "modalities": { "input": ["text", "image"], "output": ["text"] }
27
+ },
28
+ "claude-opus-4-5-thinking": {
29
+ "name": "Claude Opus 4.5 Thinking",
30
+ "limit": { "context": 200000, "output": 64000 },
31
+ "modalities": { "input": ["text", "image"], "output": ["text"] },
32
+ "variants": {
33
+ "low": { "thinkingConfig": { "thinkingBudget": 8192 } },
34
+ "medium": { "thinkingConfig": { "thinkingBudget": 16384 } },
35
+ "max": { "thinkingConfig": { "thinkingBudget": 32768 } }
36
+ }
37
+ },
38
+ "claude-sonnet-4-5": {
39
+ "name": "Claude Sonnet 4.5",
40
+ "limit": { "context": 200000, "output": 64000 },
41
+ "modalities": { "input": ["text", "image"], "output": ["text"] }
42
+ },
43
+ "claude-sonnet-4-5-thinking": {
44
+ "name": "Claude Sonnet 4.5 Thinking",
45
+ "limit": { "context": 200000, "output": 64000 },
46
+ "modalities": { "input": ["text", "image"], "output": ["text"] },
47
+ "variants": {
48
+ "low": { "thinkingConfig": { "thinkingBudget": 8192 } },
49
+ "medium": { "thinkingConfig": { "thinkingBudget": 16384 } },
50
+ "max": { "thinkingConfig": { "thinkingBudget": 32768 } }
51
+ }
52
+ },
53
+ "claude-haiku-4-5": {
54
+ "name": "Claude Haiku 4.5",
55
+ "limit": { "context": 200000, "output": 64000 },
56
+ "modalities": { "input": ["text", "image"], "output": ["text"] }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ ## Setup
65
+
66
+ 1. Run `opencode auth login`.
67
+ 2. Select `Other`, type `kiro`, and press enter.
68
+ 3. Follow the terminal instructions to complete the AWS Builder ID authentication.
69
+ 4. Configuration template will be automatically created at `~/.config/opencode/kiro.json` on first load.
70
+
71
+ ## Storage
72
+
73
+ - Credentials: `~/.config/opencode/kiro-accounts.json`
74
+ - Usage Tracking: `~/.config/opencode/kiro-usage.json`
75
+ - Plugin Config: `~/.config/opencode/kiro.json`
76
+
77
+ ## Acknowledgements
78
+
79
+ Special thanks to [AIClient-2-API](https://github.com/justlovemaki/AIClient-2-API) for providing the foundational Kiro authentication logic and request patterns.
80
+
81
+ ## Disclaimer
82
+
83
+ This plugin is provided strictly for learning and educational purposes. It is an independent implementation and is not affiliated with, endorsed by, or supported by Amazon Web Services (AWS) or Anthropic. Use of this plugin is at your own risk.
84
+
85
+ Feel free to open a PR to optimize this plugin further.
@@ -0,0 +1,26 @@
1
+ import type { KiroRegion } from './plugin/types';
2
+ export declare function isValidRegion(region: string): region is KiroRegion;
3
+ export declare function normalizeRegion(region: string | undefined): KiroRegion;
4
+ export declare function buildUrl(template: string, region: KiroRegion): string;
5
+ export declare function validateUrl(url: string): boolean;
6
+ export declare const KIRO_CONSTANTS: {
7
+ REFRESH_URL: string;
8
+ REFRESH_IDC_URL: string;
9
+ BASE_URL: string;
10
+ USAGE_LIMITS_URL: string;
11
+ DEFAULT_REGION: KiroRegion;
12
+ ACCESS_TOKEN_EXPIRY_BUFFER_MS: number;
13
+ AXIOS_TIMEOUT: number;
14
+ USER_AGENT: string;
15
+ KIRO_VERSION: string;
16
+ CHAT_TRIGGER_TYPE_MANUAL: string;
17
+ ORIGIN_AI_EDITOR: string;
18
+ };
19
+ export declare const MODEL_MAPPING: Record<string, string>;
20
+ export declare const SUPPORTED_MODELS: string[];
21
+ export declare const KIRO_AUTH_SERVICE: {
22
+ ENDPOINT: string;
23
+ SSO_OIDC_ENDPOINT: string;
24
+ BUILDER_ID_START_URL: string;
25
+ SCOPES: string[];
26
+ };
@@ -0,0 +1,60 @@
1
+ const VALID_REGIONS = ['us-east-1', 'us-west-2'];
2
+ export function isValidRegion(region) {
3
+ return VALID_REGIONS.includes(region);
4
+ }
5
+ export function normalizeRegion(region) {
6
+ if (!region || !isValidRegion(region)) {
7
+ return 'us-east-1';
8
+ }
9
+ return region;
10
+ }
11
+ export function buildUrl(template, region) {
12
+ const url = template.replace('{{region}}', region);
13
+ try {
14
+ new URL(url);
15
+ return url;
16
+ }
17
+ catch {
18
+ throw new Error(`Invalid URL generated: ${url}`);
19
+ }
20
+ }
21
+ export function validateUrl(url) {
22
+ try {
23
+ new URL(url);
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ export const KIRO_CONSTANTS = {
31
+ REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken',
32
+ REFRESH_IDC_URL: 'https://oidc.{{region}}.amazonaws.com/token',
33
+ BASE_URL: 'https://q.{{region}}.amazonaws.com/generateAssistantResponse',
34
+ USAGE_LIMITS_URL: 'https://q.{{region}}.amazonaws.com/getUsageLimits',
35
+ DEFAULT_REGION: 'us-east-1',
36
+ ACCESS_TOKEN_EXPIRY_BUFFER_MS: 60000,
37
+ AXIOS_TIMEOUT: 120000,
38
+ USER_AGENT: 'KiroIDE',
39
+ KIRO_VERSION: '0.7.5',
40
+ CHAT_TRIGGER_TYPE_MANUAL: 'MANUAL',
41
+ ORIGIN_AI_EDITOR: 'AI_EDITOR'
42
+ };
43
+ export const MODEL_MAPPING = {
44
+ 'claude-opus-4-5': 'claude-opus-4.5',
45
+ 'claude-opus-4-5-thinking': 'claude-opus-4.5',
46
+ 'claude-opus-4-5-20251101': 'claude-opus-4.5',
47
+ 'claude-haiku-4-5': 'claude-haiku-4.5',
48
+ 'claude-sonnet-4-5': 'CLAUDE_SONNET_4_5_20250929_V1_0',
49
+ 'claude-sonnet-4-5-thinking': 'CLAUDE_SONNET_4_5_20250929_V1_0',
50
+ 'claude-sonnet-4-5-20250929': 'CLAUDE_SONNET_4_5_20250929_V1_0',
51
+ 'claude-sonnet-4-20250514': 'CLAUDE_SONNET_4_20250514_V1_0',
52
+ 'claude-3-7-sonnet-20250219': 'CLAUDE_3_7_SONNET_20250219_V1_0'
53
+ };
54
+ export const SUPPORTED_MODELS = Object.keys(MODEL_MAPPING);
55
+ export const KIRO_AUTH_SERVICE = {
56
+ ENDPOINT: 'https://prod.{{region}}.auth.desktop.kiro.dev',
57
+ SSO_OIDC_ENDPOINT: 'https://oidc.{{region}}.amazonaws.com',
58
+ BUILDER_ID_START_URL: 'https://view.awsapps.com/start',
59
+ SCOPES: ['codewhisperer:completions', 'codewhisperer:analysis', 'codewhisperer:conversations', 'codewhisperer:transformations', 'codewhisperer:taskassist']
60
+ };
@@ -0,0 +1,3 @@
1
+ export { KiroOAuthPlugin } from './plugin.js';
2
+ export type { KiroConfig } from './plugin/config/index.js';
3
+ export type { KiroRegion, KiroAuthMethod, ManagedAccount } from './plugin/types.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { KiroOAuthPlugin } from './plugin.js';
@@ -0,0 +1,5 @@
1
+ import type { KiroAuthDetails, RefreshParts } from '../plugin/types';
2
+ export declare function decodeRefreshToken(refresh: string): RefreshParts;
3
+ export declare function accessTokenExpired(auth: KiroAuthDetails): boolean;
4
+ export declare function validateAuthDetails(auth: KiroAuthDetails): boolean;
5
+ export declare function encodeRefreshToken(parts: RefreshParts): string;
@@ -0,0 +1,24 @@
1
+ import { KIRO_CONSTANTS } from '../constants';
2
+ export function decodeRefreshToken(refresh) {
3
+ const parts = refresh.split('|');
4
+ if (parts.length < 2)
5
+ return { refreshToken: parts[0], authMethod: 'idc' };
6
+ const refreshToken = parts[0];
7
+ const authMethod = parts[parts.length - 1];
8
+ if (authMethod === 'idc')
9
+ return { refreshToken, clientId: parts[1], clientSecret: parts[2], authMethod: 'idc' };
10
+ return { refreshToken, authMethod: 'idc' };
11
+ }
12
+ export function accessTokenExpired(auth) {
13
+ if (!auth.access || !auth.expires)
14
+ return true;
15
+ return Date.now() >= auth.expires - KIRO_CONSTANTS.ACCESS_TOKEN_EXPIRY_BUFFER_MS;
16
+ }
17
+ export function validateAuthDetails(auth) {
18
+ return !!auth.refresh && auth.authMethod === 'idc' && !!auth.clientId && !!auth.clientSecret;
19
+ }
20
+ export function encodeRefreshToken(parts) {
21
+ if (!parts.clientId || !parts.clientSecret)
22
+ throw new Error('Missing credentials');
23
+ return `${parts.refreshToken}|${parts.clientId}|${parts.clientSecret}|idc`;
24
+ }
@@ -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,132 @@
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
+ const registerResponse = await fetch(`${ssoOIDCEndpoint}/client/register`, {
6
+ method: 'POST',
7
+ headers: {
8
+ 'Content-Type': 'application/json',
9
+ 'User-Agent': KIRO_CONSTANTS.USER_AGENT
10
+ },
11
+ body: JSON.stringify({
12
+ clientName: 'Kiro IDE',
13
+ clientType: 'public',
14
+ scopes: KIRO_AUTH_SERVICE.SCOPES,
15
+ grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token']
16
+ })
17
+ });
18
+ if (!registerResponse.ok) {
19
+ const errorText = await registerResponse.text().catch(() => '');
20
+ throw new Error(`Client registration failed: ${registerResponse.status} ${errorText}`);
21
+ }
22
+ const registerData = await registerResponse.json();
23
+ const { clientId, clientSecret } = registerData;
24
+ if (!clientId || !clientSecret) {
25
+ throw new Error('Client registration response missing clientId or clientSecret');
26
+ }
27
+ const deviceAuthResponse = await fetch(`${ssoOIDCEndpoint}/device_authorization`, {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ 'User-Agent': KIRO_CONSTANTS.USER_AGENT
32
+ },
33
+ body: JSON.stringify({
34
+ clientId,
35
+ clientSecret,
36
+ startUrl: KIRO_AUTH_SERVICE.BUILDER_ID_START_URL
37
+ })
38
+ });
39
+ if (!deviceAuthResponse.ok) {
40
+ const errorText = await deviceAuthResponse.text().catch(() => '');
41
+ throw new Error(`Device authorization failed: ${deviceAuthResponse.status} ${errorText}`);
42
+ }
43
+ const deviceAuthData = await deviceAuthResponse.json();
44
+ const { verificationUri, verificationUriComplete, userCode, deviceCode, interval = 5, expiresIn = 600 } = deviceAuthData;
45
+ if (!deviceCode || !userCode || !verificationUri || !verificationUriComplete) {
46
+ throw new Error('Device authorization response missing required fields');
47
+ }
48
+ return {
49
+ verificationUrl: verificationUri,
50
+ verificationUriComplete,
51
+ userCode,
52
+ deviceCode,
53
+ clientId,
54
+ clientSecret,
55
+ interval,
56
+ expiresIn,
57
+ region: effectiveRegion
58
+ };
59
+ }
60
+ export async function pollKiroIDCToken(clientId, clientSecret, deviceCode, interval, expiresIn, region) {
61
+ if (!clientId || !clientSecret || !deviceCode) {
62
+ throw new Error('Missing required parameters for token polling');
63
+ }
64
+ const effectiveRegion = normalizeRegion(region);
65
+ const ssoOIDCEndpoint = buildUrl(KIRO_AUTH_SERVICE.SSO_OIDC_ENDPOINT, effectiveRegion);
66
+ const maxAttempts = Math.floor(expiresIn / interval);
67
+ let currentInterval = interval * 1000;
68
+ let attempts = 0;
69
+ while (attempts < maxAttempts) {
70
+ attempts++;
71
+ await new Promise((resolve) => setTimeout(resolve, currentInterval));
72
+ try {
73
+ const tokenResponse = await fetch(`${ssoOIDCEndpoint}/token`, {
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ 'User-Agent': KIRO_CONSTANTS.USER_AGENT
78
+ },
79
+ body: JSON.stringify({
80
+ clientId,
81
+ clientSecret,
82
+ deviceCode,
83
+ grantType: 'urn:ietf:params:oauth:grant-type:device_code'
84
+ })
85
+ });
86
+ const tokenData = await tokenResponse.json();
87
+ if (tokenData.error) {
88
+ const errorType = tokenData.error;
89
+ if (errorType === 'authorization_pending') {
90
+ continue;
91
+ }
92
+ if (errorType === 'slow_down') {
93
+ currentInterval += 5000;
94
+ continue;
95
+ }
96
+ if (errorType === 'expired_token') {
97
+ throw new Error('Device code has expired. Please restart the authorization process.');
98
+ }
99
+ if (errorType === 'access_denied') {
100
+ throw new Error('Authorization was denied by the user.');
101
+ }
102
+ throw new Error(`Token polling failed: ${errorType} - ${tokenData.error_description || ''}`);
103
+ }
104
+ if (tokenData.accessToken && tokenData.refreshToken) {
105
+ const expiresInSeconds = tokenData.expiresIn || 3600;
106
+ const expiresAt = Date.now() + expiresInSeconds * 1000;
107
+ return {
108
+ refreshToken: tokenData.refreshToken,
109
+ accessToken: tokenData.accessToken,
110
+ expiresAt,
111
+ email: 'builder-id@aws.amazon.com',
112
+ clientId,
113
+ clientSecret,
114
+ region: effectiveRegion,
115
+ authMethod: 'idc'
116
+ };
117
+ }
118
+ if (!tokenResponse.ok) {
119
+ throw new Error(`Token request failed with status: ${tokenResponse.status}`);
120
+ }
121
+ }
122
+ catch (error) {
123
+ if (error instanceof Error && (error.message.includes('expired') || error.message.includes('denied') || error.message.includes('failed'))) {
124
+ throw error;
125
+ }
126
+ if (attempts >= maxAttempts) {
127
+ throw new Error(`Token polling failed after ${attempts} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`);
128
+ }
129
+ }
130
+ }
131
+ throw new Error('Token polling timed out. Authorization may have expired.');
132
+ }
@@ -0,0 +1,17 @@
1
+ import type { KiroRegion } from '../plugin/types';
2
+ export interface KiroSocialAuthorization {
3
+ url: string;
4
+ verifier: string;
5
+ region: KiroRegion;
6
+ }
7
+ export interface KiroSocialTokenResult {
8
+ refreshToken: string;
9
+ accessToken: string;
10
+ expiresAt: number;
11
+ email: string;
12
+ profileArn: string;
13
+ region: KiroRegion;
14
+ authMethod: 'social';
15
+ }
16
+ export declare function authorizeKiroSocial(port: number, state: string, challenge: string, region?: KiroRegion): Promise<string>;
17
+ export declare function exchangeKiroSocial(code: string, verifier: string, region: KiroRegion, port: number): Promise<KiroSocialTokenResult>;
@@ -0,0 +1,51 @@
1
+ import { KIRO_AUTH_SERVICE, KIRO_CONSTANTS } from '../constants';
2
+ export async function authorizeKiroSocial(port, state, challenge, region) {
3
+ const effectiveRegion = region || KIRO_CONSTANTS.DEFAULT_REGION;
4
+ const authServiceEndpoint = KIRO_AUTH_SERVICE.ENDPOINT.replace('{{region}}', effectiveRegion);
5
+ const redirectUri = `http://127.0.0.1:${port}/oauth/callback`;
6
+ const params = new URLSearchParams({
7
+ idp: 'Google',
8
+ redirect_uri: redirectUri,
9
+ code_challenge: challenge,
10
+ code_challenge_method: 'S256',
11
+ state: state,
12
+ prompt: 'select_account',
13
+ });
14
+ return `${authServiceEndpoint}/login?${params.toString()}`;
15
+ }
16
+ export async function exchangeKiroSocial(code, verifier, region, port) {
17
+ const authServiceEndpoint = KIRO_AUTH_SERVICE.ENDPOINT.replace('{{region}}', region);
18
+ const redirectUri = `http://127.0.0.1:${port}/oauth/callback`;
19
+ const tokenResponse = await fetch(`${authServiceEndpoint}/oauth/token`, {
20
+ method: 'POST',
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ 'User-Agent': KIRO_CONSTANTS.USER_AGENT,
24
+ },
25
+ body: JSON.stringify({
26
+ code,
27
+ code_verifier: verifier,
28
+ redirect_uri: redirectUri,
29
+ }),
30
+ });
31
+ if (!tokenResponse.ok) {
32
+ const errorText = await tokenResponse.text();
33
+ throw new Error(`Token exchange failed: ${tokenResponse.status} ${errorText}`);
34
+ }
35
+ const tokenData = await tokenResponse.json();
36
+ if (!tokenData.accessToken || !tokenData.refreshToken || !tokenData.profileArn) {
37
+ throw new Error('Invalid token response: missing required fields');
38
+ }
39
+ const expiresIn = tokenData.expiresIn || 3600;
40
+ const expiresAt = Date.now() + expiresIn * 1000;
41
+ const email = tokenData.email || 'unknown@kiro.dev';
42
+ return {
43
+ refreshToken: tokenData.refreshToken,
44
+ accessToken: tokenData.accessToken,
45
+ expiresAt,
46
+ email,
47
+ profileArn: tokenData.profileArn,
48
+ region,
49
+ authMethod: 'social',
50
+ };
51
+ }
@@ -0,0 +1,28 @@
1
+ import type { ManagedAccount, AccountSelectionStrategy, KiroAuthDetails, UsageMetadata } from './types';
2
+ export declare function generateAccountId(): string;
3
+ export declare class AccountManager {
4
+ private accounts;
5
+ private usage;
6
+ private cursor;
7
+ private strategy;
8
+ private lastToastTime;
9
+ constructor(accounts: ManagedAccount[], usage: Record<string, UsageMetadata>, strategy?: AccountSelectionStrategy);
10
+ static loadFromDisk(strategy?: AccountSelectionStrategy): Promise<AccountManager>;
11
+ getAccountCount(): number;
12
+ getAccounts(): ManagedAccount[];
13
+ shouldShowToast(debounce?: number): boolean;
14
+ getMinWaitTime(): number;
15
+ getCurrentOrNext(): ManagedAccount | null;
16
+ updateUsage(id: string, meta: {
17
+ usedCount: number;
18
+ limitCount: number;
19
+ realEmail?: string;
20
+ }): void;
21
+ addAccount(a: ManagedAccount): void;
22
+ removeAccount(a: ManagedAccount): void;
23
+ updateFromAuth(a: ManagedAccount, auth: KiroAuthDetails): void;
24
+ markRateLimited(a: ManagedAccount, ms: number): void;
25
+ markUnhealthy(a: ManagedAccount, reason: string, recovery?: number): void;
26
+ saveToDisk(): Promise<void>;
27
+ toAuthDetails(a: ManagedAccount): KiroAuthDetails;
28
+ }
@@ -0,0 +1,156 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { loadAccounts, saveAccounts, loadUsage, saveUsage } from './storage';
3
+ import { KIRO_CONSTANTS } from '../constants';
4
+ import { encodeRefreshToken, decodeRefreshToken } from '../kiro/auth';
5
+ export function generateAccountId() {
6
+ return randomBytes(16).toString('hex');
7
+ }
8
+ export class AccountManager {
9
+ accounts;
10
+ usage;
11
+ cursor;
12
+ strategy;
13
+ lastToastTime = 0;
14
+ constructor(accounts, usage, strategy = 'sticky') {
15
+ this.accounts = accounts;
16
+ this.usage = usage;
17
+ this.cursor = 0;
18
+ this.strategy = strategy;
19
+ for (const a of this.accounts) {
20
+ const m = this.usage[a.id];
21
+ if (m) {
22
+ a.usedCount = m.usedCount;
23
+ a.limitCount = m.limitCount;
24
+ a.realEmail = m.realEmail;
25
+ }
26
+ }
27
+ }
28
+ static async loadFromDisk(strategy) {
29
+ const s = await loadAccounts();
30
+ const u = await loadUsage();
31
+ const accounts = s.accounts.map((m) => ({ ...m, region: m.region || KIRO_CONSTANTS.DEFAULT_REGION }));
32
+ return new AccountManager(accounts, u.usage, strategy || 'sticky');
33
+ }
34
+ getAccountCount() {
35
+ return this.accounts.length;
36
+ }
37
+ getAccounts() {
38
+ return [...this.accounts];
39
+ }
40
+ shouldShowToast(debounce = 30000) {
41
+ if (Date.now() - this.lastToastTime < debounce)
42
+ return false;
43
+ this.lastToastTime = Date.now();
44
+ return true;
45
+ }
46
+ getMinWaitTime() {
47
+ const now = Date.now();
48
+ const waits = this.accounts.map((a) => (a.rateLimitResetTime || 0) - now).filter((t) => t > 0);
49
+ return waits.length > 0 ? Math.min(...waits) : 0;
50
+ }
51
+ getCurrentOrNext() {
52
+ const now = Date.now();
53
+ const available = this.accounts.filter((a) => {
54
+ if (!a.isHealthy) {
55
+ if (a.recoveryTime && now >= a.recoveryTime) {
56
+ a.isHealthy = true;
57
+ delete a.unhealthyReason;
58
+ delete a.recoveryTime;
59
+ return true;
60
+ }
61
+ return false;
62
+ }
63
+ return !(a.rateLimitResetTime && now < a.rateLimitResetTime);
64
+ });
65
+ if (available.length === 0)
66
+ return null;
67
+ let selected;
68
+ if (this.strategy === 'sticky') {
69
+ selected = available.find((_, i) => i === this.cursor) || available[0];
70
+ }
71
+ else if (this.strategy === 'round-robin') {
72
+ selected = available[this.cursor % available.length];
73
+ this.cursor = (this.cursor + 1) % available.length;
74
+ }
75
+ else if (this.strategy === 'lowest-usage') {
76
+ selected = [...available].sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
77
+ }
78
+ if (selected) {
79
+ selected.lastUsed = now;
80
+ selected.usedCount = (selected.usedCount || 0) + 1;
81
+ this.cursor = this.accounts.indexOf(selected);
82
+ return selected;
83
+ }
84
+ return null;
85
+ }
86
+ updateUsage(id, meta) {
87
+ const a = this.accounts.find((x) => x.id === id);
88
+ if (a) {
89
+ a.usedCount = meta.usedCount;
90
+ a.limitCount = meta.limitCount;
91
+ if (meta.realEmail)
92
+ a.realEmail = meta.realEmail;
93
+ }
94
+ this.usage[id] = { ...meta, lastSync: Date.now() };
95
+ }
96
+ addAccount(a) {
97
+ const i = this.accounts.findIndex((x) => x.id === a.id);
98
+ if (i === -1)
99
+ this.accounts.push(a);
100
+ else
101
+ this.accounts[i] = a;
102
+ }
103
+ removeAccount(a) {
104
+ this.accounts = this.accounts.filter((x) => x.id !== a.id);
105
+ delete this.usage[a.id];
106
+ this.cursor = Math.max(0, Math.min(this.cursor, this.accounts.length - 1));
107
+ }
108
+ updateFromAuth(a, auth) {
109
+ const acc = this.accounts.find((x) => x.id === a.id);
110
+ if (acc) {
111
+ acc.accessToken = auth.access;
112
+ acc.expiresAt = auth.expires;
113
+ acc.lastUsed = Date.now();
114
+ if (auth.email && auth.email !== 'builder-id@aws.amazon.com')
115
+ acc.realEmail = auth.email;
116
+ const p = decodeRefreshToken(auth.refresh);
117
+ acc.refreshToken = p.refreshToken;
118
+ if (p.profileArn)
119
+ acc.profileArn = p.profileArn;
120
+ if (p.clientId)
121
+ acc.clientId = p.clientId;
122
+ }
123
+ }
124
+ markRateLimited(a, ms) {
125
+ const acc = this.accounts.find((x) => x.id === a.id);
126
+ if (acc)
127
+ acc.rateLimitResetTime = Date.now() + ms;
128
+ }
129
+ markUnhealthy(a, reason, recovery) {
130
+ const acc = this.accounts.find((x) => x.id === a.id);
131
+ if (acc) {
132
+ acc.isHealthy = false;
133
+ acc.unhealthyReason = reason;
134
+ acc.recoveryTime = recovery;
135
+ }
136
+ }
137
+ async saveToDisk() {
138
+ const metadata = this.accounts.map(({ usedCount, limitCount, lastUsed, ...rest }) => rest);
139
+ await saveAccounts({ version: 1, accounts: metadata, activeIndex: this.cursor });
140
+ await saveUsage({ version: 1, usage: this.usage });
141
+ }
142
+ toAuthDetails(a) {
143
+ const p = { refreshToken: a.refreshToken, profileArn: a.profileArn, clientId: a.clientId, clientSecret: a.clientSecret, authMethod: a.authMethod };
144
+ return {
145
+ refresh: encodeRefreshToken(p),
146
+ access: a.accessToken,
147
+ expires: a.expiresAt,
148
+ authMethod: a.authMethod,
149
+ region: a.region,
150
+ profileArn: a.profileArn,
151
+ clientId: a.clientId,
152
+ clientSecret: a.clientSecret,
153
+ email: a.email
154
+ };
155
+ }
156
+ }
@@ -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;