@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,266 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { decodeRefreshToken, encodeRefreshToken } from '../kiro/auth.js';
3
+ import { isPermanentError } from './health.js';
4
+ import * as logger from './logger.js';
5
+ import { kiroDb } from './storage/sqlite.js';
6
+ import { writeToKiroCli } from './sync/kiro-cli.js';
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
+ oidcRegion: r.oidc_region || undefined,
31
+ clientId: r.client_id,
32
+ clientSecret: r.client_secret,
33
+ profileArn: r.profile_arn,
34
+ startUrl: r.start_url || undefined,
35
+ refreshToken: r.refresh_token,
36
+ accessToken: r.access_token,
37
+ expiresAt: r.expires_at,
38
+ rateLimitResetTime: r.rate_limit_reset,
39
+ isHealthy: r.is_healthy === 1,
40
+ unhealthyReason: r.unhealthy_reason,
41
+ recoveryTime: r.recovery_time,
42
+ failCount: r.fail_count || 0,
43
+ lastUsed: r.last_used,
44
+ usedCount: r.used_count,
45
+ limitCount: r.limit_count
46
+ }));
47
+ return new AccountManager(accounts, strategy || 'sticky');
48
+ }
49
+ getAccountCount() {
50
+ return this.accounts.length;
51
+ }
52
+ getAccounts() {
53
+ return [...this.accounts];
54
+ }
55
+ shouldShowToast(debounce = 10000) {
56
+ if (Date.now() - this.lastToastTime < debounce)
57
+ return false;
58
+ this.lastToastTime = Date.now();
59
+ return true;
60
+ }
61
+ shouldShowUsageToast(debounce = 10000) {
62
+ if (Date.now() - this.lastUsageToastTime < debounce)
63
+ return false;
64
+ this.lastUsageToastTime = Date.now();
65
+ return true;
66
+ }
67
+ getMinWaitTime() {
68
+ const now = Date.now();
69
+ const waits = this.accounts.map((a) => (a.rateLimitResetTime || 0) - now).filter((t) => t > 0);
70
+ return waits.length > 0 ? Math.min(...waits) : 0;
71
+ }
72
+ getCurrentOrNext() {
73
+ const now = Date.now();
74
+ const available = this.accounts.filter((a) => {
75
+ if (!a.isHealthy) {
76
+ if (isPermanentError(a.unhealthyReason)) {
77
+ return false;
78
+ }
79
+ if (a.failCount < 10 && a.recoveryTime && now >= a.recoveryTime) {
80
+ a.isHealthy = true;
81
+ delete a.unhealthyReason;
82
+ delete a.recoveryTime;
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+ return !(a.rateLimitResetTime && now < a.rateLimitResetTime);
88
+ });
89
+ let selected;
90
+ if (available.length > 0) {
91
+ if (this.strategy === 'sticky') {
92
+ selected = available.find((_, i) => i === this.cursor) || available[0];
93
+ }
94
+ else if (this.strategy === 'round-robin') {
95
+ selected = available[this.cursor % available.length];
96
+ this.cursor = (this.cursor + 1) % available.length;
97
+ }
98
+ else if (this.strategy === 'lowest-usage') {
99
+ selected = [...available].sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
100
+ }
101
+ }
102
+ if (!selected) {
103
+ const fallback = this.accounts
104
+ .filter((a) => !a.isHealthy && a.failCount < 10 && !isPermanentError(a.unhealthyReason))
105
+ .sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
106
+ if (fallback) {
107
+ fallback.isHealthy = true;
108
+ delete fallback.unhealthyReason;
109
+ delete fallback.recoveryTime;
110
+ selected = fallback;
111
+ }
112
+ }
113
+ if (selected) {
114
+ selected.lastUsed = now;
115
+ selected.usedCount = (selected.usedCount || 0) + 1;
116
+ this.cursor = this.accounts.indexOf(selected);
117
+ return selected;
118
+ }
119
+ return null;
120
+ }
121
+ updateUsage(id, meta) {
122
+ const a = this.accounts.find((x) => x.id === id);
123
+ if (a) {
124
+ a.usedCount = meta.usedCount;
125
+ a.limitCount = meta.limitCount;
126
+ if (meta.email)
127
+ a.email = meta.email;
128
+ if (!isPermanentError(a.unhealthyReason)) {
129
+ a.failCount = 0;
130
+ a.isHealthy = true;
131
+ delete a.unhealthyReason;
132
+ delete a.recoveryTime;
133
+ }
134
+ kiroDb.upsertAccount(a).catch((e) => logger.warn('DB write failed', {
135
+ method: 'updateUsage',
136
+ email: a.email,
137
+ error: e instanceof Error ? e.message : String(e)
138
+ }));
139
+ }
140
+ }
141
+ addAccount(a) {
142
+ const i = this.accounts.findIndex((x) => x.id === a.id);
143
+ if (i === -1)
144
+ this.accounts.push(a);
145
+ else
146
+ this.accounts[i] = a;
147
+ kiroDb.upsertAccount(a).catch((e) => logger.warn('DB write failed', {
148
+ method: 'addAccount',
149
+ email: a.email,
150
+ error: e instanceof Error ? e.message : String(e)
151
+ }));
152
+ }
153
+ removeAccount(a) {
154
+ const removedIndex = this.accounts.findIndex((x) => x.id === a.id);
155
+ if (removedIndex === -1)
156
+ return;
157
+ this.accounts = this.accounts.filter((x) => x.id !== a.id);
158
+ kiroDb.deleteAccount(a.id).catch((e) => logger.warn('DB write failed', {
159
+ method: 'removeAccount',
160
+ email: a.email,
161
+ error: e instanceof Error ? e.message : String(e)
162
+ }));
163
+ if (this.accounts.length === 0)
164
+ this.cursor = 0;
165
+ else if (this.cursor >= this.accounts.length)
166
+ this.cursor = this.accounts.length - 1;
167
+ else if (removedIndex <= this.cursor && this.cursor > 0)
168
+ this.cursor--;
169
+ }
170
+ updateFromAuth(a, auth) {
171
+ const acc = this.accounts.find((x) => x.id === a.id);
172
+ if (acc) {
173
+ acc.accessToken = auth.access;
174
+ acc.expiresAt = auth.expires;
175
+ acc.lastUsed = Date.now();
176
+ if (auth.email)
177
+ acc.email = auth.email;
178
+ const p = decodeRefreshToken(auth.refresh);
179
+ acc.refreshToken = p.refreshToken;
180
+ if (p.profileArn)
181
+ acc.profileArn = p.profileArn;
182
+ if (p.clientId)
183
+ acc.clientId = p.clientId;
184
+ acc.failCount = 0;
185
+ acc.isHealthy = true;
186
+ delete acc.unhealthyReason;
187
+ delete acc.recoveryTime;
188
+ kiroDb.upsertAccount(acc).catch((e) => logger.warn('DB write failed', {
189
+ method: 'updateFromAuth',
190
+ email: acc.email,
191
+ error: e instanceof Error ? e.message : String(e)
192
+ }));
193
+ writeToKiroCli(acc).catch((e) => logger.warn('CLI write failed', {
194
+ method: 'updateFromAuth',
195
+ email: acc.email,
196
+ error: e instanceof Error ? e.message : String(e)
197
+ }));
198
+ }
199
+ }
200
+ markRateLimited(a, ms) {
201
+ const acc = this.accounts.find((x) => x.id === a.id);
202
+ if (acc) {
203
+ acc.rateLimitResetTime = Date.now() + ms;
204
+ kiroDb.upsertAccount(acc).catch((e) => logger.warn('DB write failed', {
205
+ method: 'markRateLimited',
206
+ email: acc.email,
207
+ error: e instanceof Error ? e.message : String(e)
208
+ }));
209
+ }
210
+ }
211
+ markUnhealthy(a, reason, recovery) {
212
+ const acc = this.accounts.find((x) => x.id === a.id);
213
+ if (!acc)
214
+ return;
215
+ const isPermanent = isPermanentError(reason);
216
+ if (isPermanent) {
217
+ logger.warn('Account marked as permanently unhealthy', {
218
+ email: acc.email,
219
+ reason,
220
+ accountId: acc.id
221
+ });
222
+ acc.failCount = 10;
223
+ acc.isHealthy = false;
224
+ acc.unhealthyReason = reason;
225
+ delete acc.recoveryTime;
226
+ }
227
+ else {
228
+ acc.failCount = (acc.failCount || 0) + 1;
229
+ acc.unhealthyReason = reason;
230
+ acc.lastUsed = Date.now();
231
+ if (acc.failCount >= 10) {
232
+ acc.isHealthy = false;
233
+ acc.recoveryTime = recovery || Date.now() + 3600000;
234
+ }
235
+ }
236
+ kiroDb.upsertAccount(acc).catch((e) => logger.warn('DB write failed', {
237
+ method: 'markUnhealthy',
238
+ email: acc.email,
239
+ error: e instanceof Error ? e.message : String(e)
240
+ }));
241
+ }
242
+ async saveToDisk() {
243
+ await kiroDb.batchUpsertAccounts(this.accounts);
244
+ }
245
+ toAuthDetails(a) {
246
+ const p = {
247
+ refreshToken: a.refreshToken,
248
+ profileArn: a.profileArn,
249
+ clientId: a.clientId,
250
+ clientSecret: a.clientSecret,
251
+ authMethod: a.authMethod
252
+ };
253
+ return {
254
+ refresh: encodeRefreshToken(p),
255
+ access: a.accessToken,
256
+ expires: a.expiresAt,
257
+ authMethod: a.authMethod,
258
+ region: a.region,
259
+ oidcRegion: a.oidcRegion,
260
+ profileArn: a.profileArn,
261
+ clientId: a.clientId,
262
+ clientSecret: a.clientSecret,
263
+ email: a.email
264
+ };
265
+ }
266
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * OpenCode only calls the auth loader when there is a stored auth entry for the
3
+ * provider in auth.json. The plugin syncs credentials from the Kiro IDE's local
4
+ * SQLite database, so it doesn't need the user to go through an OAuth flow first.
5
+ *
6
+ * This writes a minimal placeholder entry into auth.json so OpenCode calls the
7
+ * loader on the next startup, where real credentials are synced from Kiro CLI DB.
8
+ */
9
+ export declare function bootstrapAuthIfNeeded(providerId: string): void;
@@ -0,0 +1,69 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import * as logger from './logger.js';
5
+ import { getCliDbPath } from './sync/kiro-cli-parser.js';
6
+ function getOpenCodeAuthPath() {
7
+ const dataRoot = process.platform === 'win32'
8
+ ? process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local')
9
+ : process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
10
+ return join(dataRoot, 'opencode', 'auth.json');
11
+ }
12
+ function readAuthFile(authPath) {
13
+ if (!existsSync(authPath))
14
+ return {};
15
+ try {
16
+ const parsed = JSON.parse(readFileSync(authPath, 'utf-8'));
17
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
18
+ logger.warn('Bootstrap: auth.json is not an object, skipping placeholder auth setup');
19
+ return null;
20
+ }
21
+ return parsed;
22
+ }
23
+ catch (e) {
24
+ logger.warn(`Bootstrap: invalid auth.json, skipping placeholder auth setup: ${e instanceof Error ? e.message : String(e)}`);
25
+ return null;
26
+ }
27
+ }
28
+ function writeAuthFile(authPath, auth) {
29
+ mkdirSync(dirname(authPath), { recursive: true });
30
+ const mode = existsSync(authPath) ? statSync(authPath).mode & 0o777 : 0o600;
31
+ const tempPath = `${authPath}.${process.pid}.${Date.now()}.tmp`;
32
+ writeFileSync(tempPath, JSON.stringify(auth, null, 2), { encoding: 'utf-8', mode });
33
+ chmodSync(tempPath, mode);
34
+ renameSync(tempPath, authPath);
35
+ }
36
+ /**
37
+ * OpenCode only calls the auth loader when there is a stored auth entry for the
38
+ * provider in auth.json. The plugin syncs credentials from the Kiro IDE's local
39
+ * SQLite database, so it doesn't need the user to go through an OAuth flow first.
40
+ *
41
+ * This writes a minimal placeholder entry into auth.json so OpenCode calls the
42
+ * loader on the next startup, where real credentials are synced from Kiro CLI DB.
43
+ */
44
+ export function bootstrapAuthIfNeeded(providerId) {
45
+ try {
46
+ const cliDbPath = getCliDbPath();
47
+ if (!existsSync(cliDbPath)) {
48
+ logger.log('Bootstrap: Kiro CLI DB not found, skipping');
49
+ return;
50
+ }
51
+ const authPath = getOpenCodeAuthPath();
52
+ const auth = readAuthFile(authPath);
53
+ if (!auth)
54
+ return;
55
+ if (auth[providerId]) {
56
+ return;
57
+ }
58
+ logger.log(`Bootstrap: writing placeholder auth entry for provider "${providerId}"`);
59
+ auth[providerId] = {
60
+ type: 'api',
61
+ key: 'kiro-bootstrap-placeholder'
62
+ };
63
+ writeAuthFile(authPath, auth);
64
+ logger.log('Bootstrap: auth.json updated — loader will run on next request');
65
+ }
66
+ catch (e) {
67
+ logger.warn(`Bootstrap failed: ${e instanceof Error ? e.message : String(e)}`);
68
+ }
69
+ }
@@ -0,0 +1,4 @@
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;
4
+ export declare function getStartUrlInputHtml(defaultStartUrl: string, submitUrl: string): string;