@sunerpy/opencode-kiro-auth 0.1.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 (113) hide show
  1. package/LICENSE +678 -0
  2. package/README.md +270 -0
  3. package/dist/constants.d.ts +28 -0
  4. package/dist/constants.js +108 -0
  5. package/dist/core/account/account-selector.d.ts +25 -0
  6. package/dist/core/account/account-selector.js +80 -0
  7. package/dist/core/account/usage-tracker.d.ts +20 -0
  8. package/dist/core/account/usage-tracker.js +56 -0
  9. package/dist/core/auth/auth-handler.d.ts +18 -0
  10. package/dist/core/auth/auth-handler.js +236 -0
  11. package/dist/core/auth/idc-auth-method.d.ts +9 -0
  12. package/dist/core/auth/idc-auth-method.js +169 -0
  13. package/dist/core/auth/token-refresher.d.ts +23 -0
  14. package/dist/core/auth/token-refresher.js +85 -0
  15. package/dist/core/index.d.ts +9 -0
  16. package/dist/core/index.js +9 -0
  17. package/dist/core/request/error-handler.d.ts +32 -0
  18. package/dist/core/request/error-handler.js +159 -0
  19. package/dist/core/request/request-handler.d.ts +35 -0
  20. package/dist/core/request/request-handler.js +296 -0
  21. package/dist/core/request/response-handler.d.ts +8 -0
  22. package/dist/core/request/response-handler.js +137 -0
  23. package/dist/core/request/retry-strategy.d.ts +18 -0
  24. package/dist/core/request/retry-strategy.js +28 -0
  25. package/dist/index.d.ts +94 -0
  26. package/dist/index.js +2 -0
  27. package/dist/infrastructure/database/account-cache.d.ts +14 -0
  28. package/dist/infrastructure/database/account-cache.js +44 -0
  29. package/dist/infrastructure/database/account-repository.d.ts +12 -0
  30. package/dist/infrastructure/database/account-repository.js +66 -0
  31. package/dist/infrastructure/index.d.ts +7 -0
  32. package/dist/infrastructure/index.js +7 -0
  33. package/dist/infrastructure/transformers/event-stream-parser.d.ts +7 -0
  34. package/dist/infrastructure/transformers/event-stream-parser.js +115 -0
  35. package/dist/infrastructure/transformers/history-builder.d.ts +16 -0
  36. package/dist/infrastructure/transformers/history-builder.js +226 -0
  37. package/dist/infrastructure/transformers/message-transformer.d.ts +5 -0
  38. package/dist/infrastructure/transformers/message-transformer.js +99 -0
  39. package/dist/infrastructure/transformers/tool-call-parser.d.ts +4 -0
  40. package/dist/infrastructure/transformers/tool-call-parser.js +45 -0
  41. package/dist/infrastructure/transformers/tool-transformer.d.ts +2 -0
  42. package/dist/infrastructure/transformers/tool-transformer.js +19 -0
  43. package/dist/kiro/auth.d.ts +4 -0
  44. package/dist/kiro/auth.js +25 -0
  45. package/dist/kiro/oauth-idc.d.ts +25 -0
  46. package/dist/kiro/oauth-idc.js +167 -0
  47. package/dist/plugin/accounts.d.ts +29 -0
  48. package/dist/plugin/accounts.js +266 -0
  49. package/dist/plugin/auth-bootstrap.d.ts +9 -0
  50. package/dist/plugin/auth-bootstrap.js +69 -0
  51. package/dist/plugin/auth-page.d.ts +4 -0
  52. package/dist/plugin/auth-page.js +721 -0
  53. package/dist/plugin/config/index.d.ts +3 -0
  54. package/dist/plugin/config/index.js +2 -0
  55. package/dist/plugin/config/loader.d.ts +6 -0
  56. package/dist/plugin/config/loader.js +129 -0
  57. package/dist/plugin/config/schema.d.ts +88 -0
  58. package/dist/plugin/config/schema.js +94 -0
  59. package/dist/plugin/effort.d.ts +46 -0
  60. package/dist/plugin/effort.js +113 -0
  61. package/dist/plugin/errors.d.ts +17 -0
  62. package/dist/plugin/errors.js +34 -0
  63. package/dist/plugin/health.d.ts +1 -0
  64. package/dist/plugin/health.js +13 -0
  65. package/dist/plugin/image-handler.d.ts +18 -0
  66. package/dist/plugin/image-handler.js +82 -0
  67. package/dist/plugin/logger.d.ts +8 -0
  68. package/dist/plugin/logger.js +84 -0
  69. package/dist/plugin/models.d.ts +2 -0
  70. package/dist/plugin/models.js +11 -0
  71. package/dist/plugin/request.d.ts +9 -0
  72. package/dist/plugin/request.js +287 -0
  73. package/dist/plugin/response.d.ts +3 -0
  74. package/dist/plugin/response.js +97 -0
  75. package/dist/plugin/sdk-client.d.ts +4 -0
  76. package/dist/plugin/sdk-client.js +55 -0
  77. package/dist/plugin/storage/locked-operations.d.ts +5 -0
  78. package/dist/plugin/storage/locked-operations.js +103 -0
  79. package/dist/plugin/storage/migrations.d.ts +4 -0
  80. package/dist/plugin/storage/migrations.js +155 -0
  81. package/dist/plugin/storage/sqlite.d.ts +18 -0
  82. package/dist/plugin/storage/sqlite.js +166 -0
  83. package/dist/plugin/streaming/index.d.ts +2 -0
  84. package/dist/plugin/streaming/index.js +2 -0
  85. package/dist/plugin/streaming/openai-converter.d.ts +2 -0
  86. package/dist/plugin/streaming/openai-converter.js +81 -0
  87. package/dist/plugin/streaming/sdk-stream-transformer.d.ts +1 -0
  88. package/dist/plugin/streaming/sdk-stream-transformer.js +274 -0
  89. package/dist/plugin/streaming/stream-parser.d.ts +5 -0
  90. package/dist/plugin/streaming/stream-parser.js +136 -0
  91. package/dist/plugin/streaming/stream-state.d.ts +5 -0
  92. package/dist/plugin/streaming/stream-state.js +59 -0
  93. package/dist/plugin/streaming/stream-transformer.d.ts +1 -0
  94. package/dist/plugin/streaming/stream-transformer.js +295 -0
  95. package/dist/plugin/streaming/types.d.ts +25 -0
  96. package/dist/plugin/streaming/types.js +2 -0
  97. package/dist/plugin/sync/kiro-cli-parser.d.ts +8 -0
  98. package/dist/plugin/sync/kiro-cli-parser.js +72 -0
  99. package/dist/plugin/sync/kiro-cli-profile.d.ts +1 -0
  100. package/dist/plugin/sync/kiro-cli-profile.js +30 -0
  101. package/dist/plugin/sync/kiro-cli.d.ts +2 -0
  102. package/dist/plugin/sync/kiro-cli.js +212 -0
  103. package/dist/plugin/sync/stale-accounts.d.ts +9 -0
  104. package/dist/plugin/sync/stale-accounts.js +31 -0
  105. package/dist/plugin/token.d.ts +2 -0
  106. package/dist/plugin/token.js +78 -0
  107. package/dist/plugin/types.d.ts +125 -0
  108. package/dist/plugin/types.js +0 -0
  109. package/dist/plugin/usage.d.ts +3 -0
  110. package/dist/plugin/usage.js +79 -0
  111. package/dist/plugin.d.ts +174 -0
  112. package/dist/plugin.js +170 -0
  113. package/package.json +74 -0
@@ -0,0 +1,236 @@
1
+ import { RegionSchema } from '../../plugin/config/schema.js';
2
+ import * as logger from '../../plugin/logger.js';
3
+ import { IdcAuthMethod } from './idc-auth-method.js';
4
+ export class AuthHandler {
5
+ config;
6
+ repository;
7
+ accountManager;
8
+ constructor(config, repository) {
9
+ this.config = config;
10
+ this.repository = repository;
11
+ }
12
+ async initialize(showToast) {
13
+ const { syncFromKiroCli } = await import('../../plugin/sync/kiro-cli.js');
14
+ logger.log('Auth init', { autoSyncKiroCli: !!this.config.auto_sync_kiro_cli });
15
+ if (this.config.auto_sync_kiro_cli) {
16
+ logger.log('Kiro CLI sync: start');
17
+ await syncFromKiroCli();
18
+ this.repository.invalidateCache();
19
+ const accounts = await this.repository.findAll();
20
+ if (this.accountManager) {
21
+ for (const a of accounts)
22
+ this.accountManager.addAccount(a);
23
+ }
24
+ logger.log('Kiro CLI sync: done', { importedAccounts: accounts.length });
25
+ }
26
+ this.logUsageSummary(showToast);
27
+ }
28
+ logUsageSummary(showToast) {
29
+ if (!this.accountManager)
30
+ return;
31
+ const accounts = this.accountManager.getAccounts();
32
+ if (!accounts.length)
33
+ return;
34
+ for (const acc of accounts) {
35
+ const used = acc.usedCount ?? 0;
36
+ const limit = acc.limitCount ?? 0;
37
+ if (limit > 0) {
38
+ const pct = Math.round((used / limit) * 100);
39
+ const msg = `Kiro usage (${acc.email}): ${used}/${limit} (${pct}%)`;
40
+ logger.log(msg);
41
+ if (showToast) {
42
+ const variant = pct >= 90 ? 'warning' : 'info';
43
+ setTimeout(() => showToast(msg, variant), 3000);
44
+ }
45
+ }
46
+ else if (used > 0) {
47
+ const msg = `Kiro usage (${acc.email}): ${used} requests used`;
48
+ logger.log(msg);
49
+ if (showToast)
50
+ setTimeout(() => showToast(msg, 'info'), 3000);
51
+ }
52
+ }
53
+ }
54
+ setAccountManager(am) {
55
+ this.accountManager = am;
56
+ }
57
+ /** Summarize stored accounts for a label; guards limit=0 divide-by-zero. */
58
+ buildUsageSummary(accounts) {
59
+ if (!accounts.length)
60
+ return '';
61
+ const CAP = 3;
62
+ const parts = accounts.slice(0, CAP).map((acc) => {
63
+ const email = acc.email || 'unknown';
64
+ const used = acc.usedCount ?? 0;
65
+ const limit = acc.limitCount ?? 0;
66
+ if (limit > 0) {
67
+ const pct = Math.round((used / limit) * 100);
68
+ return `${email} ${used}/${limit} (${pct}%)`;
69
+ }
70
+ return `${email} ${used} used`;
71
+ });
72
+ const remaining = accounts.length - Math.min(accounts.length, CAP);
73
+ const body = remaining > 0 ? `${parts.join(' · ')} +${remaining} more` : parts.join(' · ');
74
+ return `[current: ${body}]`;
75
+ }
76
+ /** Format a single account as a select-option label for the remove flow. */
77
+ formatAccountOption(acc) {
78
+ const email = acc.email || 'unknown';
79
+ const used = acc.usedCount ?? 0;
80
+ const limit = acc.limitCount ?? 0;
81
+ const region = acc.region || 'unknown-region';
82
+ const health = acc.isHealthy ? 'healthy' : 'unhealthy';
83
+ return `${email} — ${used}/${limit} (${region}, ${health})`;
84
+ }
85
+ getMethods() {
86
+ if (!this.accountManager) {
87
+ return [];
88
+ }
89
+ const currentAccounts = this.accountManager.getAccounts?.() ?? [];
90
+ const usageSummary = this.buildUsageSummary(currentAccounts);
91
+ const firstLabelBase = 'Add account · AWS Builder ID / IAM Identity Center';
92
+ const firstLabel = usageSummary ? `${firstLabelBase} ${usageSummary}` : firstLabelBase;
93
+ const idcMethod = new IdcAuthMethod(this.config, this.repository, this.accountManager);
94
+ const configStartUrl = this.config.idc_start_url;
95
+ const configRegion = this.config.idc_region;
96
+ const methods = [
97
+ {
98
+ label: firstLabel,
99
+ type: 'oauth',
100
+ prompts: [
101
+ {
102
+ type: 'text',
103
+ key: 'start_url',
104
+ message: configStartUrl
105
+ ? `IAM Identity Center Start URL (current: ${configStartUrl}, leave blank to keep)`
106
+ : 'IAM Identity Center Start URL (leave blank for AWS Builder ID)',
107
+ placeholder: 'https://your-company.awsapps.com/start',
108
+ validate: (value) => {
109
+ if (!value)
110
+ return undefined;
111
+ try {
112
+ new URL(value);
113
+ return undefined;
114
+ }
115
+ catch {
116
+ return 'Please enter a valid URL';
117
+ }
118
+ }
119
+ },
120
+ {
121
+ type: 'text',
122
+ key: 'idc_region',
123
+ message: configRegion && configRegion !== 'us-east-1'
124
+ ? `IAM Identity Center region (sso_region) (current: ${configRegion}, leave blank to keep)`
125
+ : 'IAM Identity Center region (sso_region) (leave blank for us-east-1)',
126
+ placeholder: 'us-east-1',
127
+ validate: (value) => {
128
+ if (!value)
129
+ return undefined;
130
+ return RegionSchema.safeParse(value.trim()).success
131
+ ? undefined
132
+ : 'Please enter a valid AWS region';
133
+ }
134
+ }
135
+ ],
136
+ authorize: (inputs) => idcMethod.authorize(inputs)
137
+ },
138
+ {
139
+ label: 'Add account · IAM Identity Center (with Profile ARN)',
140
+ type: 'oauth',
141
+ prompts: [
142
+ {
143
+ type: 'text',
144
+ key: 'start_url',
145
+ message: configStartUrl
146
+ ? `IAM Identity Center Start URL (current: ${configStartUrl}, leave blank to keep)`
147
+ : 'IAM Identity Center Start URL (leave blank for AWS Builder ID)',
148
+ placeholder: 'https://your-company.awsapps.com/start',
149
+ validate: (value) => {
150
+ if (!value)
151
+ return undefined;
152
+ try {
153
+ new URL(value);
154
+ return undefined;
155
+ }
156
+ catch {
157
+ return 'Please enter a valid URL';
158
+ }
159
+ }
160
+ },
161
+ {
162
+ type: 'text',
163
+ key: 'idc_region',
164
+ message: configRegion && configRegion !== 'us-east-1'
165
+ ? `IAM Identity Center region (sso_region) (current: ${configRegion}, leave blank to keep)`
166
+ : 'IAM Identity Center region (sso_region) (leave blank for us-east-1)',
167
+ placeholder: 'us-east-1',
168
+ validate: (value) => {
169
+ if (!value)
170
+ return undefined;
171
+ return RegionSchema.safeParse(value.trim()).success
172
+ ? undefined
173
+ : 'Please enter a valid AWS region';
174
+ }
175
+ },
176
+ {
177
+ type: 'text',
178
+ key: 'profile_arn',
179
+ message: this.config.idc_profile_arn
180
+ ? `Profile ARN (current: ${this.config.idc_profile_arn}, leave blank to keep)`
181
+ : 'Profile ARN (e.g. arn:aws:codewhisperer:eu-central-1:428597928572:profile/HE7XVERQ9VXW)',
182
+ placeholder: 'arn:aws:codewhisperer:us-east-1:123456789012:profile/XXXXXXXXXX',
183
+ validate: (value) => {
184
+ if (!value && this.config.idc_profile_arn)
185
+ return undefined;
186
+ if (!value)
187
+ return 'Profile ARN is required for this method';
188
+ return value.startsWith('arn:aws:codewhisperer:') ||
189
+ value.startsWith('arn:aws:qdeveloper:')
190
+ ? undefined
191
+ : 'Please enter a valid CodeWhisperer or Q Developer profile ARN';
192
+ }
193
+ }
194
+ ],
195
+ authorize: (inputs) => idcMethod.authorize(inputs)
196
+ }
197
+ ];
198
+ const count = currentAccounts.length;
199
+ const removeLabel = count > 0 ? `Remove account · ${count} stored` : 'Remove account · none stored';
200
+ const removeOptions = currentAccounts.map((acc) => ({
201
+ label: this.formatAccountOption(acc),
202
+ value: String(acc.id)
203
+ }));
204
+ removeOptions.push({ label: 'Cancel', value: '__cancel__' });
205
+ methods.push({
206
+ label: removeLabel,
207
+ type: 'api',
208
+ prompts: [
209
+ {
210
+ type: 'select',
211
+ key: 'account_id',
212
+ message: 'Select the account to remove',
213
+ options: removeOptions
214
+ }
215
+ ],
216
+ authorize: async (inputs) => {
217
+ const accountId = inputs?.account_id;
218
+ if (!accountId || accountId === '__cancel__') {
219
+ logger.log('Remove Kiro account: cancelled (no-op)');
220
+ return { type: 'failed' };
221
+ }
222
+ const target = currentAccounts.find((acc) => String(acc.id) === accountId);
223
+ if (!target) {
224
+ logger.warn('Remove Kiro account: id not found', { accountId });
225
+ return { type: 'failed' };
226
+ }
227
+ this.accountManager.removeAccount(target);
228
+ logger.log('Removed Kiro account', { email: target.email, accountId });
229
+ // Return 'failed' after the deletion side-effect: 'success' would make
230
+ // OpenCode persist a bogus auth.json credential for a removal action.
231
+ return { type: 'failed' };
232
+ }
233
+ });
234
+ return methods;
235
+ }
236
+ }
@@ -0,0 +1,9 @@
1
+ import type { AuthOuathResult } from '@opencode-ai/plugin';
2
+ import type { AccountRepository } from '../../infrastructure/database/account-repository.js';
3
+ export declare class IdcAuthMethod {
4
+ private config;
5
+ private repository;
6
+ private accountManager;
7
+ constructor(config: any, repository: AccountRepository, accountManager: any);
8
+ authorize(inputs?: Record<string, string>): Promise<AuthOuathResult>;
9
+ }
@@ -0,0 +1,169 @@
1
+ import { exec } from 'node:child_process';
2
+ import { extractRegionFromArn, normalizeRegion } from '../../constants.js';
3
+ import { authorizeKiroIDC, pollKiroIDCToken } from '../../kiro/oauth-idc.js';
4
+ import { createDeterministicAccountId } from '../../plugin/accounts.js';
5
+ import * as logger from '../../plugin/logger.js';
6
+ import { makePlaceholderEmail } from '../../plugin/sync/kiro-cli-parser.js';
7
+ import { readActiveProfileArnFromKiroCli } from '../../plugin/sync/kiro-cli-profile.js';
8
+ import { fetchUsageLimits } from '../../plugin/usage.js';
9
+ const openBrowser = (url) => {
10
+ const escapedUrl = url.replace(/"/g, '\\"');
11
+ const platform = process.platform;
12
+ const cmd = platform === 'win32'
13
+ ? `cmd /c start "" "${escapedUrl}"`
14
+ : platform === 'darwin'
15
+ ? `open "${escapedUrl}"`
16
+ : `xdg-open "${escapedUrl}"`;
17
+ exec(cmd, (error) => {
18
+ if (error)
19
+ logger.warn(`Browser error: ${error.message}`);
20
+ });
21
+ };
22
+ function normalizeStartUrl(raw) {
23
+ if (!raw)
24
+ return undefined;
25
+ const trimmed = raw.trim();
26
+ if (!trimmed)
27
+ return undefined;
28
+ const url = new URL(trimmed);
29
+ url.hash = '';
30
+ url.search = '';
31
+ // Normalize common portal URL shapes to end in `/start` (AWS Builder ID and IAM Identity Center)
32
+ if (url.pathname.endsWith('/start/'))
33
+ url.pathname = url.pathname.replace(/\/start\/$/, '/start');
34
+ if (!url.pathname.endsWith('/start'))
35
+ url.pathname = url.pathname.replace(/\/+$/, '') + '/start';
36
+ return url.toString();
37
+ }
38
+ function buildDeviceUrl(startUrl, userCode) {
39
+ const url = new URL(startUrl);
40
+ url.search = '';
41
+ // Prefer `/start/` (with trailing slash) to match AWS portal URLs like `/start/#/device?...`.
42
+ if (url.pathname.endsWith('/start'))
43
+ url.pathname = `${url.pathname}/`;
44
+ url.pathname = url.pathname.replace(/\/start\/?$/, '/start/');
45
+ url.hash = `#/device?user_code=${encodeURIComponent(userCode)}`;
46
+ return url.toString();
47
+ }
48
+ export class IdcAuthMethod {
49
+ config;
50
+ repository;
51
+ accountManager;
52
+ constructor(config, repository, accountManager) {
53
+ this.config = config;
54
+ this.repository = repository;
55
+ this.accountManager = accountManager;
56
+ }
57
+ async authorize(inputs) {
58
+ const configuredServiceRegion = this.config.default_region;
59
+ const invokedWithoutPrompts = !inputs || Object.keys(inputs).length === 0;
60
+ const startUrl = normalizeStartUrl(inputs?.start_url || this.config.idc_start_url) || undefined;
61
+ const oidcRegion = normalizeRegion(inputs?.idc_region || this.config.idc_region);
62
+ const configuredProfileArn = this.config.idc_profile_arn;
63
+ logger.log('IDC authorize: resolved defaults', {
64
+ hasInputs: !!inputs && Object.keys(inputs).length > 0,
65
+ invokedWithoutPrompts,
66
+ startUrlSource: inputs?.start_url ? 'inputs' : this.config.idc_start_url ? 'config' : 'none',
67
+ oidcRegion,
68
+ startUrl: startUrl ? new URL(startUrl).origin : undefined
69
+ });
70
+ // Step 1: get device code + verification URL (fast)
71
+ const auth = await authorizeKiroIDC(oidcRegion, startUrl);
72
+ // If a custom Identity Center start URL is provided, prefer the portal device page.
73
+ // This avoids the AWS Builder ID device page (which often prompts for an email)
74
+ // and routes the user into their org's IAM Identity Center sign-in.
75
+ const verificationUrl = startUrl
76
+ ? buildDeviceUrl(startUrl, auth.userCode)
77
+ : auth.verificationUriComplete || auth.verificationUrl;
78
+ // Open the *AWS* verification page directly (no local web server).
79
+ openBrowser(verificationUrl);
80
+ return {
81
+ url: verificationUrl,
82
+ instructions: `Open the verification URL and complete sign-in.\nCode: ${auth.userCode}`,
83
+ method: 'auto',
84
+ callback: async () => {
85
+ try {
86
+ // Step 2: poll until token is issued (standard device-code flow)
87
+ const token = await pollKiroIDCToken(auth.clientId, auth.clientSecret, auth.deviceCode, auth.interval, auth.expiresIn, oidcRegion);
88
+ const profileArn = inputs?.profile_arn?.trim() || configuredProfileArn || readActiveProfileArnFromKiroCli();
89
+ const serviceRegion = extractRegionFromArn(profileArn) || configuredServiceRegion;
90
+ let usage = { usedCount: 0, limitCount: 0, email: undefined };
91
+ try {
92
+ usage = await fetchUsageLimits({
93
+ refresh: '',
94
+ access: token.accessToken,
95
+ expires: token.expiresAt,
96
+ authMethod: 'idc',
97
+ region: serviceRegion,
98
+ clientId: token.clientId,
99
+ clientSecret: token.clientSecret,
100
+ profileArn
101
+ });
102
+ }
103
+ catch (e) {
104
+ logger.warn('fetchUsageLimits failed during auth', {
105
+ error: e instanceof Error ? e.message : String(e)
106
+ });
107
+ if (startUrl && !profileArn) {
108
+ throw new Error(`Missing profile ARN for IAM Identity Center. Set "idc_profile_arn" in ~/.config/opencode/kiro.json, or run "kiro-cli profile" once so it can be auto-detected. Original error: ${e instanceof Error ? e.message : String(e)}`);
109
+ }
110
+ const errMsg = e instanceof Error ? e.message : String(e);
111
+ if (errMsg.includes('FEATURE_NOT_SUPPORTED')) {
112
+ logger.warn('fetchUsageLimits returned FEATURE_NOT_SUPPORTED; skipping usage check', {
113
+ serviceRegion,
114
+ profileArn
115
+ });
116
+ usage = {
117
+ usedCount: 0,
118
+ limitCount: 0,
119
+ email: undefined
120
+ };
121
+ }
122
+ else {
123
+ throw e;
124
+ }
125
+ }
126
+ if (!usage.email) {
127
+ try {
128
+ const tokenParts = token.accessToken.split('.');
129
+ if (tokenParts.length === 3 && tokenParts[1]) {
130
+ const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString());
131
+ usage.email = payload.email || payload.sub;
132
+ }
133
+ }
134
+ catch { }
135
+ }
136
+ const email = usage.email || makePlaceholderEmail('idc', serviceRegion, token.clientId, profileArn);
137
+ const id = createDeterministicAccountId(email, 'idc', token.clientId, profileArn);
138
+ const acc = {
139
+ id,
140
+ email,
141
+ authMethod: 'idc',
142
+ region: serviceRegion,
143
+ oidcRegion,
144
+ clientId: token.clientId,
145
+ clientSecret: token.clientSecret,
146
+ profileArn,
147
+ startUrl: startUrl || undefined,
148
+ refreshToken: token.refreshToken,
149
+ accessToken: token.accessToken,
150
+ expiresAt: token.expiresAt,
151
+ rateLimitResetTime: 0,
152
+ isHealthy: true,
153
+ failCount: 0,
154
+ usedCount: usage.usedCount,
155
+ limitCount: usage.limitCount
156
+ };
157
+ await this.repository.save(acc);
158
+ this.accountManager?.addAccount?.(acc);
159
+ return { type: 'success', key: token.accessToken };
160
+ }
161
+ catch (e) {
162
+ const err = e instanceof Error ? e : new Error(String(e));
163
+ logger.error('IDC auth callback failed', err);
164
+ throw new Error(`IDC authorization failed: ${err.message}. Check ~/.config/opencode/kiro-logs/plugin.log for details. If this is an Identity Center account, ensure you have selected an AWS Q Developer/CodeWhisperer profile (try: kiro-cli profile).`);
165
+ }
166
+ }
167
+ };
168
+ }
169
+ }
@@ -0,0 +1,23 @@
1
+ import type { AccountRepository } from '../../infrastructure/database/account-repository.js';
2
+ import type { AccountManager } from '../../plugin/accounts.js';
3
+ import type { KiroAuthDetails, ManagedAccount } from '../../plugin/types.js';
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
+ forceRefresh(account: ManagedAccount, showToast: ToastFunction): Promise<boolean>;
21
+ private handleRefreshError;
22
+ }
23
+ export {};
@@ -0,0 +1,85 @@
1
+ import { accessTokenExpired } from '../../kiro/auth.js';
2
+ import { KiroTokenRefreshError } from '../../plugin/errors.js';
3
+ import * as logger from '../../plugin/logger.js';
4
+ import { refreshAccessToken } from '../../plugin/token.js';
5
+ export class TokenRefresher {
6
+ config;
7
+ accountManager;
8
+ syncFromKiroCli;
9
+ repository;
10
+ constructor(config, accountManager, syncFromKiroCli, repository) {
11
+ this.config = config;
12
+ this.accountManager = accountManager;
13
+ this.syncFromKiroCli = syncFromKiroCli;
14
+ this.repository = repository;
15
+ }
16
+ async refreshIfNeeded(account, auth, showToast) {
17
+ if (!accessTokenExpired(auth, this.config.token_expiry_buffer_ms)) {
18
+ return { account, shouldContinue: false };
19
+ }
20
+ try {
21
+ const newAuth = await refreshAccessToken(auth);
22
+ this.accountManager.updateFromAuth(account, newAuth);
23
+ await this.repository.batchSave(this.accountManager.getAccounts());
24
+ return { account, shouldContinue: false };
25
+ }
26
+ catch (e) {
27
+ return await this.handleRefreshError(e, account, showToast);
28
+ }
29
+ }
30
+ async forceRefresh(account, showToast) {
31
+ const auth = this.accountManager.toAuthDetails(account);
32
+ try {
33
+ const newAuth = await refreshAccessToken(auth);
34
+ this.accountManager.updateFromAuth(account, newAuth);
35
+ await this.repository.batchSave(this.accountManager.getAccounts());
36
+ return true;
37
+ }
38
+ catch (e) {
39
+ logger.error('Forced token refresh failed', {
40
+ email: account.email,
41
+ code: e instanceof KiroTokenRefreshError ? e.code : undefined,
42
+ message: e instanceof Error ? e.message : String(e)
43
+ });
44
+ showToast('403: Token refresh failed after stale-token detection.', 'warning');
45
+ return false;
46
+ }
47
+ }
48
+ async handleRefreshError(error, account, showToast) {
49
+ logger.error('Token refresh failed', {
50
+ email: account.email,
51
+ code: error instanceof KiroTokenRefreshError ? error.code : undefined,
52
+ message: error instanceof Error ? error.message : String(error)
53
+ });
54
+ if (this.config.auto_sync_kiro_cli) {
55
+ await this.syncFromKiroCli();
56
+ }
57
+ this.repository.invalidateCache();
58
+ const accounts = await this.repository.findAll();
59
+ const stillAcc = accounts.find((a) => a.id === account.id);
60
+ if (stillAcc &&
61
+ !accessTokenExpired(this.accountManager.toAuthDetails(stillAcc), this.config.token_expiry_buffer_ms)) {
62
+ showToast('Credentials recovered from Kiro CLI sync.', 'info');
63
+ return { account: stillAcc, shouldContinue: true };
64
+ }
65
+ if (error instanceof KiroTokenRefreshError &&
66
+ (error.code === 'ExpiredTokenException' ||
67
+ error.code === 'InvalidTokenException' ||
68
+ error.code === 'ExpiredClientException' ||
69
+ error.code === 'HTTP_401' ||
70
+ error.code === 'HTTP_403' ||
71
+ error.message.includes('Invalid refresh token provided') ||
72
+ error.message.includes('Invalid grant provided') ||
73
+ error.message.includes('Client is expired'))) {
74
+ this.accountManager.markUnhealthy(account, error.message);
75
+ await this.repository.batchSave(this.accountManager.getAccounts());
76
+ return { account, shouldContinue: true };
77
+ }
78
+ logger.error('Token refresh unrecoverable', {
79
+ email: account.email,
80
+ code: error instanceof KiroTokenRefreshError ? error.code : undefined,
81
+ message: error instanceof Error ? error.message : String(error)
82
+ });
83
+ throw error;
84
+ }
85
+ }
@@ -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,32 @@
1
+ import type { AccountRepository } from '../../infrastructure/database/account-repository.js';
2
+ import type { AccountManager } from '../../plugin/accounts.js';
3
+ import type { ManagedAccount } from '../../plugin/types.js';
4
+ type ToastFunction = (message: string, variant: 'info' | 'warning' | 'success' | 'error') => void;
5
+ interface RequestContext {
6
+ retry: number;
7
+ bearerRefreshAttempted?: boolean;
8
+ }
9
+ type ForceRefreshFn = (account: ManagedAccount, showToast: ToastFunction) => Promise<boolean>;
10
+ interface ErrorHandlerConfig {
11
+ rate_limit_max_retries: number;
12
+ rate_limit_retry_delay_ms: number;
13
+ }
14
+ export declare class ErrorHandler {
15
+ private config;
16
+ private accountManager;
17
+ private repository;
18
+ private forceRefresh?;
19
+ constructor(config: ErrorHandlerConfig, accountManager: AccountManager, repository: AccountRepository, forceRefresh?: ForceRefreshFn | undefined);
20
+ handle(error: any, response: Response, account: ManagedAccount, context: RequestContext, showToast: ToastFunction): Promise<{
21
+ shouldRetry: boolean;
22
+ newContext?: RequestContext;
23
+ switchAccount?: boolean;
24
+ }>;
25
+ handleNetworkError(error: any, context: RequestContext, showToast: ToastFunction): Promise<{
26
+ shouldRetry: boolean;
27
+ newContext?: RequestContext;
28
+ }>;
29
+ private isNetworkError;
30
+ private sleep;
31
+ }
32
+ export {};