@bogyie/opencode-kiro-plugin 0.3.5 → 0.3.7
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.
- package/README.md +32 -4
- package/dist/auth.d.ts +44 -2
- package/dist/auth.js +306 -13
- package/dist/config.d.ts +2 -0
- package/dist/config.js +15 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.js +70 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,13 +34,35 @@ Then add the plugin to your OpenCode config. See [examples/opencode.jsonc](examp
|
|
|
34
34
|
The plugin resolves credentials in this order:
|
|
35
35
|
|
|
36
36
|
1. `KIRO_API_KEY`
|
|
37
|
-
2. OpenCode auth input for provider `kiro
|
|
37
|
+
2. OpenCode auth input for provider `kiro`, including stored Kiro device-flow credentials
|
|
38
38
|
3. Local Kiro CLI session token from the Kiro CLI SQLite store
|
|
39
39
|
4. `kiro-cli whoami` diagnostics for CLI session visibility
|
|
40
40
|
|
|
41
|
-
When no usable Kiro CLI session is available, OpenCode's startup model discovery
|
|
41
|
+
When no usable Kiro CLI session is available, OpenCode's startup model discovery does not start browser login. If startup discovery cannot list models, the plugin keeps Kiro visible with an `auto` placeholder model.
|
|
42
42
|
|
|
43
|
-
You can
|
|
43
|
+
You can open OpenCode's provider connector, choose Kiro, and select `Kiro device login`. The connector uses AWS OIDC device authorization directly, so it does not rely on a localhost callback URL. After login succeeds, the plugin stores the access token, refresh token, OIDC client credentials, region, and optional profile ARN in OpenCode auth. Direct fetch mode reuses that stored credential and refreshes the access token before expiry; it does not ask you to log in again while the refresh token remains valid. If no OpenCode device credential or API key is configured, direct fetch reads the active Kiro CLI session token and calls Kiro's REST/EventStream endpoint directly. `cli-chat` mode uses the official `kiro-cli chat --no-interactive` surface and depends on the local Kiro CLI login state. `acp` mode uses the official `kiro-cli acp` surface, but is still treated as an explicit backend while its real-world protocol behavior is validated across Kiro CLI versions.
|
|
44
|
+
|
|
45
|
+
For AWS IAM Identity Center login, configure the default device-flow Start URL separately from the API region:
|
|
46
|
+
|
|
47
|
+
```jsonc
|
|
48
|
+
{
|
|
49
|
+
"plugin": [
|
|
50
|
+
[
|
|
51
|
+
"@bogyie/opencode-kiro-plugin",
|
|
52
|
+
{
|
|
53
|
+
"region": "ap-northeast-2",
|
|
54
|
+
"login": {
|
|
55
|
+
"license": "pro",
|
|
56
|
+
"identityProvider": "https://example.awsapps.com/start",
|
|
57
|
+
"region": "ap-northeast-2"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The connector can also prompt for the Start URL, OIDC region, and optional profile ARN. For IAM Identity Center, the plugin opens the AWS portal device URL, such as `https://example.awsapps.com/start/#/device?user_code=...`, instead of a `localhost` callback URL.
|
|
44
66
|
|
|
45
67
|
Use the `kiro_status` plugin tool to inspect provider id, backend, region, auth method, and discovered model count. Secrets are redacted in diagnostics.
|
|
46
68
|
|
|
@@ -56,6 +78,12 @@ Configure the backend through plugin options:
|
|
|
56
78
|
{
|
|
57
79
|
"backend": "auto",
|
|
58
80
|
"region": "us-east-1",
|
|
81
|
+
// Optional Kiro device login defaults:
|
|
82
|
+
// "login": {
|
|
83
|
+
// "license": "pro",
|
|
84
|
+
// "identityProvider": "https://example.awsapps.com/start",
|
|
85
|
+
// "region": "ap-northeast-2"
|
|
86
|
+
// },
|
|
59
87
|
"endpoint": "https://q.us-east-1.amazonaws.com",
|
|
60
88
|
"maxAttempts": 3,
|
|
61
89
|
"requestTimeoutMs": 120000,
|
|
@@ -127,7 +155,7 @@ This keeps new Kiro model ids usable before the package is updated without adver
|
|
|
127
155
|
|
|
128
156
|
The plugin injects `provider.kiro` automatically. You only need to add `provider.kiro.models` yourself when overriding OpenCode model-picker metadata, such as a display name or context limit. Use plugin `modelAliases` for aliases that should resolve one requested model id to another.
|
|
129
157
|
|
|
130
|
-
`modelDiscoveryCommand` defaults to `["kiro-cli", "chat", "--list-models", "--format", "json"]`. Set `modelDiscovery` to `"off"` to skip runtime discovery. If this command fails
|
|
158
|
+
`modelDiscoveryCommand` defaults to `["kiro-cli", "chat", "--list-models", "--format", "json"]`. Set `modelDiscovery` to `"off"` to skip runtime discovery. If this command fails during startup, the provider remains visible with the `auto` placeholder and you can connect Kiro through OpenCode's provider connector. Discovery stdout can be a JSON array, `{ "models": [...] }`, `{ "data": [...] }`, Kiro CLI list-models JSON, Kiro CLI plain list output, or one model id per line.
|
|
131
159
|
|
|
132
160
|
## Troubleshooting
|
|
133
161
|
|
package/dist/auth.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export interface KiroCliSessionCredentialOptions {
|
|
|
30
30
|
readonly dbPath?: string;
|
|
31
31
|
readonly tokenKeys?: ReadonlyArray<string>;
|
|
32
32
|
}
|
|
33
|
+
export type KiroAuthFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
33
34
|
export interface KiroLoginSession {
|
|
34
35
|
readonly url: string;
|
|
35
36
|
readonly code: string | undefined;
|
|
@@ -37,10 +38,41 @@ export interface KiroLoginSession {
|
|
|
37
38
|
waitForPrompt(timeoutMs?: number): Promise<boolean>;
|
|
38
39
|
waitForAuth(runner?: CommandRunner): Promise<boolean>;
|
|
39
40
|
}
|
|
41
|
+
export interface KiroCliLoginOptions {
|
|
42
|
+
readonly license?: "free" | "pro";
|
|
43
|
+
readonly identityProvider?: string;
|
|
44
|
+
readonly region?: string;
|
|
45
|
+
readonly useDeviceFlow?: boolean;
|
|
46
|
+
readonly extraArgs?: ReadonlyArray<string>;
|
|
47
|
+
}
|
|
48
|
+
export interface KiroDeviceAuthorization {
|
|
49
|
+
readonly verificationUrl: string;
|
|
50
|
+
readonly verificationUrlComplete: string;
|
|
51
|
+
readonly userCode: string;
|
|
52
|
+
readonly deviceCode: string;
|
|
53
|
+
readonly clientId: string;
|
|
54
|
+
readonly clientSecret: string;
|
|
55
|
+
readonly intervalSeconds: number;
|
|
56
|
+
readonly expiresInSeconds: number;
|
|
57
|
+
readonly oidcRegion: string;
|
|
58
|
+
readonly startUrl: string;
|
|
59
|
+
}
|
|
60
|
+
export interface KiroDeviceAuthCredential {
|
|
61
|
+
readonly accessToken: string;
|
|
62
|
+
readonly refreshToken: string;
|
|
63
|
+
readonly expiresAt: number;
|
|
64
|
+
readonly clientId: string;
|
|
65
|
+
readonly clientSecret: string;
|
|
66
|
+
readonly oidcRegion: string;
|
|
67
|
+
readonly region: string;
|
|
68
|
+
readonly startUrl: string;
|
|
69
|
+
readonly profileArn?: string;
|
|
70
|
+
}
|
|
40
71
|
export interface KiroLoginFlowOptions {
|
|
41
72
|
readonly spawner?: ProcessSpawner;
|
|
42
73
|
readonly runner?: CommandRunner;
|
|
43
74
|
readonly promptTimeoutMs?: number;
|
|
75
|
+
readonly login?: KiroCliLoginOptions;
|
|
44
76
|
}
|
|
45
77
|
export declare const KIRO_LOGIN_URL = "https://view.awsapps.com/start";
|
|
46
78
|
export declare const DEFAULT_KIRO_CLI_DB_PATH: string;
|
|
@@ -48,14 +80,24 @@ export declare const DEFAULT_KIRO_CLI_TOKEN_KEYS: readonly ["kirocli:odic:token"
|
|
|
48
80
|
export declare function runCommand(command: string, args: ReadonlyArray<string>, options?: CommandRunOptions): Promise<CommandResult>;
|
|
49
81
|
export declare function regionFromProfileArn(profileArn: string | undefined): string | undefined;
|
|
50
82
|
export declare function readKiroCliSessionCredential(options?: KiroCliSessionCredentialOptions, runner?: CommandRunner): Promise<KiroCliSessionCredential | undefined>;
|
|
83
|
+
export declare function kiroDeviceVerificationUrl(startUrl: string, userCode: string): string;
|
|
84
|
+
export declare function authorizeKiroDevice(options?: KiroCliLoginOptions, fetcher?: KiroAuthFetch): Promise<KiroDeviceAuthorization>;
|
|
85
|
+
export declare function pollKiroDeviceToken(authorization: KiroDeviceAuthorization, options: Pick<KiroDeviceAuthCredential, "region" | "profileArn">, fetcher?: KiroAuthFetch): Promise<KiroDeviceAuthCredential>;
|
|
86
|
+
export declare function refreshKiroDeviceAuthCredential(credential: KiroDeviceAuthCredential, fetcher?: KiroAuthFetch): Promise<KiroDeviceAuthCredential>;
|
|
87
|
+
export declare function encodeKiroDeviceAuthKey(credential: KiroDeviceAuthCredential): string;
|
|
88
|
+
export declare function decodeKiroDeviceAuthKey(key: string | undefined): KiroDeviceAuthCredential | undefined;
|
|
89
|
+
export declare function isKiroDeviceAuthKey(key: string | undefined): boolean;
|
|
90
|
+
export declare function credentialFromKiroDeviceAuthKey(key: string, fetcher?: KiroAuthFetch): Promise<KiroCliSessionCredential | undefined>;
|
|
51
91
|
export declare function extractKiroLoginUrl(output: string): string;
|
|
52
92
|
export declare function extractKiroLoginCode(output: string): string | undefined;
|
|
53
|
-
export declare function
|
|
54
|
-
export declare function
|
|
93
|
+
export declare function kiroCliLoginArgs(options?: KiroCliLoginOptions): string[];
|
|
94
|
+
export declare function startKiroCliLogin(optionsOrSpawner?: KiroCliLoginOptions | ProcessSpawner, spawner?: ProcessSpawner): KiroLoginSession;
|
|
95
|
+
export declare function startKiroCliLoginOnce(optionsOrSpawner?: KiroCliLoginOptions | ProcessSpawner, spawner?: ProcessSpawner): KiroLoginSession;
|
|
55
96
|
export declare function runKiroLoginFlowOnce(options?: KiroLoginFlowOptions): Promise<boolean>;
|
|
56
97
|
export declare function redacted(value: string | undefined): string | undefined;
|
|
57
98
|
export declare function detectAuth(env?: NodeJS.ProcessEnv, runner?: CommandRunner): Promise<AuthDiagnostics>;
|
|
58
99
|
export declare function resolveApiKey(auth: () => Promise<{
|
|
59
100
|
type: string;
|
|
60
101
|
key?: string;
|
|
102
|
+
access?: string;
|
|
61
103
|
}>, env?: NodeJS.ProcessEnv): Promise<string>;
|
package/dist/auth.js
CHANGED
|
@@ -8,10 +8,23 @@ const execFileAsync = promisify(execFile);
|
|
|
8
8
|
export const KIRO_LOGIN_URL = "https://view.awsapps.com/start";
|
|
9
9
|
export const DEFAULT_KIRO_CLI_DB_PATH = join(homedir(), "Library", "Application Support", "kiro-cli", "data.sqlite3");
|
|
10
10
|
export const DEFAULT_KIRO_CLI_TOKEN_KEYS = ["kirocli:odic:token", "codewhisperer:odic:token"];
|
|
11
|
+
const KIRO_DEVICE_AUTH_KEY_PREFIX = "kiro-device:";
|
|
12
|
+
const KIRO_OIDC_SCOPES = [
|
|
13
|
+
"codewhisperer:completions",
|
|
14
|
+
"codewhisperer:analysis",
|
|
15
|
+
"codewhisperer:conversations",
|
|
16
|
+
"codewhisperer:transformations",
|
|
17
|
+
"codewhisperer:taskassist",
|
|
18
|
+
];
|
|
11
19
|
const LOGIN_REUSE_WINDOW_MS = 2 * 60 * 1000;
|
|
20
|
+
const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
|
12
21
|
let sharedLoginSession;
|
|
13
22
|
let sharedLoginStartedAt = 0;
|
|
23
|
+
let sharedLoginSessionKey = "";
|
|
14
24
|
let sharedLoginFlow;
|
|
25
|
+
let sharedLoginFlowKey = "";
|
|
26
|
+
const refreshedDeviceCredentials = new Map();
|
|
27
|
+
const deviceRefreshes = new Map();
|
|
15
28
|
export async function runCommand(command, args, options = {}) {
|
|
16
29
|
try {
|
|
17
30
|
const result = await execFileAsync(command, [...args], { timeout: options.timeoutMs ?? 5000 });
|
|
@@ -39,6 +52,14 @@ function jsonObject(value) {
|
|
|
39
52
|
return undefined;
|
|
40
53
|
}
|
|
41
54
|
}
|
|
55
|
+
function numberField(input, ...keys) {
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const value = input?.[key];
|
|
58
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
42
63
|
function stringField(input, ...keys) {
|
|
43
64
|
for (const key of keys) {
|
|
44
65
|
const value = input?.[key];
|
|
@@ -96,6 +117,243 @@ export async function readKiroCliSessionCredential(options = {}, runner = runCom
|
|
|
96
117
|
function delay(ms) {
|
|
97
118
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
98
119
|
}
|
|
120
|
+
function normalizeStartUrl(raw) {
|
|
121
|
+
const value = raw?.trim() || KIRO_LOGIN_URL;
|
|
122
|
+
const url = new URL(value);
|
|
123
|
+
url.hash = "";
|
|
124
|
+
url.search = "";
|
|
125
|
+
if (url.pathname.endsWith("/start/"))
|
|
126
|
+
url.pathname = url.pathname.replace(/\/start\/$/, "/start");
|
|
127
|
+
if (!url.pathname.endsWith("/start"))
|
|
128
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/start`;
|
|
129
|
+
return url.toString();
|
|
130
|
+
}
|
|
131
|
+
export function kiroDeviceVerificationUrl(startUrl, userCode) {
|
|
132
|
+
const url = new URL(startUrl);
|
|
133
|
+
url.search = "";
|
|
134
|
+
if (url.pathname.endsWith("/start"))
|
|
135
|
+
url.pathname = `${url.pathname}/`;
|
|
136
|
+
url.pathname = url.pathname.replace(/\/start\/?$/, "/start/");
|
|
137
|
+
url.hash = `#/device?user_code=${encodeURIComponent(userCode)}`;
|
|
138
|
+
return url.toString();
|
|
139
|
+
}
|
|
140
|
+
function oidcEndpoint(region) {
|
|
141
|
+
return `https://oidc.${region || "us-east-1"}.amazonaws.com`;
|
|
142
|
+
}
|
|
143
|
+
function deviceAuthUserAgent() {
|
|
144
|
+
return "KiroIDE";
|
|
145
|
+
}
|
|
146
|
+
async function jsonResponse(response) {
|
|
147
|
+
const text = await response.text();
|
|
148
|
+
const parsed = jsonObject(text);
|
|
149
|
+
if (!parsed)
|
|
150
|
+
throw new Error(`Kiro auth returned invalid JSON: ${text.slice(0, 200)}`);
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
const message = stringField(parsed, "message", "error_description", "error") ?? text.slice(0, 200);
|
|
153
|
+
throw new Error(`Kiro auth request failed: HTTP ${response.status} ${message}`);
|
|
154
|
+
}
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
export async function authorizeKiroDevice(options = {}, fetcher = fetch) {
|
|
158
|
+
const oidcRegion = options.region || "us-east-1";
|
|
159
|
+
const startUrl = normalizeStartUrl(options.identityProvider);
|
|
160
|
+
const endpoint = oidcEndpoint(oidcRegion);
|
|
161
|
+
const register = await jsonResponse(await fetcher(`${endpoint}/client/register`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
"content-type": "application/json",
|
|
165
|
+
"user-agent": deviceAuthUserAgent(),
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
clientName: "Kiro IDE",
|
|
169
|
+
clientType: "public",
|
|
170
|
+
scopes: KIRO_OIDC_SCOPES,
|
|
171
|
+
grantTypes: ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"],
|
|
172
|
+
}),
|
|
173
|
+
}));
|
|
174
|
+
const clientId = stringField(register, "clientId", "client_id");
|
|
175
|
+
const clientSecret = stringField(register, "clientSecret", "client_secret");
|
|
176
|
+
if (!clientId || !clientSecret)
|
|
177
|
+
throw new Error("Kiro auth client registration did not return client credentials.");
|
|
178
|
+
const authorization = await jsonResponse(await fetcher(`${endpoint}/device_authorization`, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: {
|
|
181
|
+
"content-type": "application/json",
|
|
182
|
+
"user-agent": deviceAuthUserAgent(),
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
clientId,
|
|
186
|
+
clientSecret,
|
|
187
|
+
startUrl,
|
|
188
|
+
}),
|
|
189
|
+
}));
|
|
190
|
+
const verificationUrl = stringField(authorization, "verificationUri", "verification_uri");
|
|
191
|
+
const verificationUrlComplete = stringField(authorization, "verificationUriComplete", "verification_uri_complete");
|
|
192
|
+
const userCode = stringField(authorization, "userCode", "user_code");
|
|
193
|
+
const deviceCode = stringField(authorization, "deviceCode", "device_code");
|
|
194
|
+
if (!verificationUrl || !verificationUrlComplete || !userCode || !deviceCode) {
|
|
195
|
+
throw new Error("Kiro device authorization response did not return required fields.");
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
verificationUrl,
|
|
199
|
+
verificationUrlComplete,
|
|
200
|
+
userCode,
|
|
201
|
+
deviceCode,
|
|
202
|
+
clientId,
|
|
203
|
+
clientSecret,
|
|
204
|
+
intervalSeconds: numberField(authorization, "interval") ?? 5,
|
|
205
|
+
expiresInSeconds: numberField(authorization, "expiresIn", "expires_in") ?? 600,
|
|
206
|
+
oidcRegion,
|
|
207
|
+
startUrl,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export async function pollKiroDeviceToken(authorization, options, fetcher = fetch) {
|
|
211
|
+
const maxAttempts = Math.max(1, Math.floor(authorization.expiresInSeconds / authorization.intervalSeconds));
|
|
212
|
+
let intervalMs = authorization.intervalSeconds * 1000;
|
|
213
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
214
|
+
await delay(intervalMs);
|
|
215
|
+
const response = await fetcher(`${oidcEndpoint(authorization.oidcRegion)}/token`, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: {
|
|
218
|
+
"content-type": "application/json",
|
|
219
|
+
"user-agent": deviceAuthUserAgent(),
|
|
220
|
+
},
|
|
221
|
+
body: JSON.stringify({
|
|
222
|
+
clientId: authorization.clientId,
|
|
223
|
+
clientSecret: authorization.clientSecret,
|
|
224
|
+
deviceCode: authorization.deviceCode,
|
|
225
|
+
grantType: "urn:ietf:params:oauth:grant-type:device_code",
|
|
226
|
+
}),
|
|
227
|
+
});
|
|
228
|
+
const data = await response.text().then((text) => jsonObject(text) ?? {});
|
|
229
|
+
const error = stringField(data, "error");
|
|
230
|
+
if (error === "authorization_pending")
|
|
231
|
+
continue;
|
|
232
|
+
if (error === "slow_down") {
|
|
233
|
+
intervalMs += 5000;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (error) {
|
|
237
|
+
const description = stringField(data, "error_description") ?? "";
|
|
238
|
+
throw new Error(`Kiro device authorization failed: ${error}${description ? ` - ${description}` : ""}`);
|
|
239
|
+
}
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
throw new Error(`Kiro device token request failed: HTTP ${response.status}`);
|
|
242
|
+
}
|
|
243
|
+
const accessToken = stringField(data, "access_token", "accessToken");
|
|
244
|
+
const refreshToken = stringField(data, "refresh_token", "refreshToken");
|
|
245
|
+
if (!accessToken || !refreshToken)
|
|
246
|
+
throw new Error("Kiro device token response did not return access and refresh tokens.");
|
|
247
|
+
const expiresIn = numberField(data, "expires_in", "expiresIn") ?? 3600;
|
|
248
|
+
return {
|
|
249
|
+
accessToken,
|
|
250
|
+
refreshToken,
|
|
251
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
252
|
+
clientId: authorization.clientId,
|
|
253
|
+
clientSecret: authorization.clientSecret,
|
|
254
|
+
oidcRegion: authorization.oidcRegion,
|
|
255
|
+
region: options.profileArn ? (regionFromProfileArn(options.profileArn) ?? options.region) : options.region,
|
|
256
|
+
startUrl: authorization.startUrl,
|
|
257
|
+
...(options.profileArn ? { profileArn: options.profileArn } : {}),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
throw new Error("Kiro device authorization timed out.");
|
|
261
|
+
}
|
|
262
|
+
export async function refreshKiroDeviceAuthCredential(credential, fetcher = fetch) {
|
|
263
|
+
const data = await jsonResponse(await fetcher(`${oidcEndpoint(credential.oidcRegion)}/token`, {
|
|
264
|
+
method: "POST",
|
|
265
|
+
headers: {
|
|
266
|
+
"content-type": "application/json",
|
|
267
|
+
"user-agent": deviceAuthUserAgent(),
|
|
268
|
+
},
|
|
269
|
+
body: JSON.stringify({
|
|
270
|
+
refreshToken: credential.refreshToken,
|
|
271
|
+
clientId: credential.clientId,
|
|
272
|
+
clientSecret: credential.clientSecret,
|
|
273
|
+
grantType: "refresh_token",
|
|
274
|
+
}),
|
|
275
|
+
}));
|
|
276
|
+
const accessToken = stringField(data, "access_token", "accessToken");
|
|
277
|
+
if (!accessToken)
|
|
278
|
+
throw new Error("Kiro refresh token response did not return an access token.");
|
|
279
|
+
const refreshToken = stringField(data, "refresh_token", "refreshToken") ?? credential.refreshToken;
|
|
280
|
+
const expiresIn = numberField(data, "expires_in", "expiresIn") ?? 3600;
|
|
281
|
+
return {
|
|
282
|
+
...credential,
|
|
283
|
+
accessToken,
|
|
284
|
+
refreshToken,
|
|
285
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function base64UrlEncode(value) {
|
|
289
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
290
|
+
}
|
|
291
|
+
function base64UrlDecode(value) {
|
|
292
|
+
return Buffer.from(value, "base64url").toString("utf8");
|
|
293
|
+
}
|
|
294
|
+
export function encodeKiroDeviceAuthKey(credential) {
|
|
295
|
+
return `${KIRO_DEVICE_AUTH_KEY_PREFIX}${base64UrlEncode(JSON.stringify(credential))}`;
|
|
296
|
+
}
|
|
297
|
+
export function decodeKiroDeviceAuthKey(key) {
|
|
298
|
+
try {
|
|
299
|
+
if (!key?.startsWith(KIRO_DEVICE_AUTH_KEY_PREFIX))
|
|
300
|
+
return undefined;
|
|
301
|
+
const parsed = jsonObject(base64UrlDecode(key.slice(KIRO_DEVICE_AUTH_KEY_PREFIX.length)));
|
|
302
|
+
const accessToken = stringField(parsed, "accessToken");
|
|
303
|
+
const refreshToken = stringField(parsed, "refreshToken");
|
|
304
|
+
const expiresAt = numberField(parsed, "expiresAt");
|
|
305
|
+
const clientId = stringField(parsed, "clientId");
|
|
306
|
+
const clientSecret = stringField(parsed, "clientSecret");
|
|
307
|
+
const oidcRegion = stringField(parsed, "oidcRegion");
|
|
308
|
+
const region = stringField(parsed, "region");
|
|
309
|
+
const startUrl = stringField(parsed, "startUrl");
|
|
310
|
+
if (!accessToken || !refreshToken || !expiresAt || !clientId || !clientSecret || !oidcRegion || !region || !startUrl)
|
|
311
|
+
return undefined;
|
|
312
|
+
const profileArn = stringField(parsed, "profileArn");
|
|
313
|
+
return {
|
|
314
|
+
accessToken,
|
|
315
|
+
refreshToken,
|
|
316
|
+
expiresAt,
|
|
317
|
+
clientId,
|
|
318
|
+
clientSecret,
|
|
319
|
+
oidcRegion,
|
|
320
|
+
region,
|
|
321
|
+
startUrl,
|
|
322
|
+
...(profileArn ? { profileArn } : {}),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
export function isKiroDeviceAuthKey(key) {
|
|
330
|
+
return Boolean(decodeKiroDeviceAuthKey(key));
|
|
331
|
+
}
|
|
332
|
+
export async function credentialFromKiroDeviceAuthKey(key, fetcher = fetch) {
|
|
333
|
+
const cached = refreshedDeviceCredentials.get(key);
|
|
334
|
+
let credential = cached ?? decodeKiroDeviceAuthKey(key);
|
|
335
|
+
if (!credential)
|
|
336
|
+
return undefined;
|
|
337
|
+
if (credential.expiresAt <= Date.now() + TOKEN_REFRESH_BUFFER_MS) {
|
|
338
|
+
let refresh = deviceRefreshes.get(key);
|
|
339
|
+
if (!refresh) {
|
|
340
|
+
refresh = refreshKiroDeviceAuthCredential(credential, fetcher).finally(() => {
|
|
341
|
+
deviceRefreshes.delete(key);
|
|
342
|
+
});
|
|
343
|
+
deviceRefreshes.set(key, refresh);
|
|
344
|
+
}
|
|
345
|
+
credential = await refresh;
|
|
346
|
+
refreshedDeviceCredentials.set(key, credential);
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
accessToken: credential.accessToken,
|
|
350
|
+
refreshToken: credential.refreshToken,
|
|
351
|
+
expiresAt: new Date(credential.expiresAt).toISOString(),
|
|
352
|
+
...(credential.profileArn ? { profileArn: credential.profileArn } : {}),
|
|
353
|
+
region: regionFromProfileArn(credential.profileArn) ?? credential.region,
|
|
354
|
+
source: "opencode-device-auth",
|
|
355
|
+
};
|
|
356
|
+
}
|
|
99
357
|
function urls(text) {
|
|
100
358
|
return text.match(/https?:\/\/[^\s"'<>]+/g) ?? [];
|
|
101
359
|
}
|
|
@@ -153,8 +411,33 @@ export function extractKiroLoginUrl(output) {
|
|
|
153
411
|
export function extractKiroLoginCode(output) {
|
|
154
412
|
return /\b[A-Z0-9]{4}-[A-Z0-9]{4}\b/.exec(output)?.[0] ?? /\b[A-Z0-9]{8,}\b/.exec(output)?.[0];
|
|
155
413
|
}
|
|
156
|
-
|
|
157
|
-
|
|
414
|
+
function defaultSpawner(command, args) {
|
|
415
|
+
return spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
416
|
+
}
|
|
417
|
+
function loginKey(options = {}) {
|
|
418
|
+
return JSON.stringify(kiroCliLoginArgs(options));
|
|
419
|
+
}
|
|
420
|
+
function loginOptionsAndSpawner(optionsOrSpawner, spawner) {
|
|
421
|
+
if (typeof optionsOrSpawner === "function")
|
|
422
|
+
return { options: {}, spawner: optionsOrSpawner };
|
|
423
|
+
return { options: optionsOrSpawner ?? {}, spawner: spawner ?? defaultSpawner };
|
|
424
|
+
}
|
|
425
|
+
export function kiroCliLoginArgs(options = {}) {
|
|
426
|
+
const args = ["login"];
|
|
427
|
+
if (options.license)
|
|
428
|
+
args.push("--license", options.license);
|
|
429
|
+
if (options.identityProvider)
|
|
430
|
+
args.push("--identity-provider", options.identityProvider);
|
|
431
|
+
if (options.region)
|
|
432
|
+
args.push("--region", options.region);
|
|
433
|
+
if (options.useDeviceFlow)
|
|
434
|
+
args.push("--use-device-flow");
|
|
435
|
+
args.push(...(options.extraArgs ?? []));
|
|
436
|
+
return args;
|
|
437
|
+
}
|
|
438
|
+
export function startKiroCliLogin(optionsOrSpawner, spawner) {
|
|
439
|
+
const resolved = loginOptionsAndSpawner(optionsOrSpawner, spawner);
|
|
440
|
+
const child = resolved.spawner("kiro-cli", kiroCliLoginArgs(resolved.options));
|
|
158
441
|
let output = "";
|
|
159
442
|
let exited = false;
|
|
160
443
|
let exitCode = null;
|
|
@@ -206,11 +489,14 @@ export function startKiroCliLogin(spawner = (command, args) => spawn(command, [.
|
|
|
206
489
|
},
|
|
207
490
|
};
|
|
208
491
|
}
|
|
209
|
-
export function startKiroCliLoginOnce(
|
|
492
|
+
export function startKiroCliLoginOnce(optionsOrSpawner, spawner) {
|
|
493
|
+
const resolved = loginOptionsAndSpawner(optionsOrSpawner, spawner);
|
|
494
|
+
const key = loginKey(resolved.options);
|
|
210
495
|
const now = Date.now();
|
|
211
|
-
if (sharedLoginSession && now - sharedLoginStartedAt < LOGIN_REUSE_WINDOW_MS)
|
|
496
|
+
if (sharedLoginSession && sharedLoginSessionKey === key && now - sharedLoginStartedAt < LOGIN_REUSE_WINDOW_MS) {
|
|
212
497
|
return sharedLoginSession;
|
|
213
|
-
|
|
498
|
+
}
|
|
499
|
+
const session = startKiroCliLogin(resolved.options, resolved.spawner);
|
|
214
500
|
const wrapped = {
|
|
215
501
|
get url() {
|
|
216
502
|
return session.url;
|
|
@@ -232,22 +518,29 @@ export function startKiroCliLoginOnce(spawner = (command, args) => spawn(command
|
|
|
232
518
|
if (sharedLoginSession === wrapped) {
|
|
233
519
|
sharedLoginSession = undefined;
|
|
234
520
|
sharedLoginStartedAt = 0;
|
|
521
|
+
sharedLoginSessionKey = "";
|
|
235
522
|
}
|
|
236
523
|
}
|
|
237
524
|
},
|
|
238
525
|
};
|
|
239
526
|
sharedLoginSession = wrapped;
|
|
240
527
|
sharedLoginStartedAt = now;
|
|
528
|
+
sharedLoginSessionKey = key;
|
|
241
529
|
return wrapped;
|
|
242
530
|
}
|
|
243
531
|
export async function runKiroLoginFlowOnce(options = {}) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
532
|
+
const key = loginKey(options.login);
|
|
533
|
+
if (!sharedLoginFlow || sharedLoginFlowKey !== key) {
|
|
534
|
+
sharedLoginFlowKey = key;
|
|
535
|
+
sharedLoginFlow = (async () => {
|
|
536
|
+
const session = startKiroCliLoginOnce(options.login, options.spawner);
|
|
537
|
+
await session.waitForPrompt(options.promptTimeoutMs);
|
|
538
|
+
return session.waitForAuth(options.runner);
|
|
539
|
+
})().finally(() => {
|
|
540
|
+
sharedLoginFlow = undefined;
|
|
541
|
+
sharedLoginFlowKey = "";
|
|
542
|
+
});
|
|
543
|
+
}
|
|
251
544
|
return sharedLoginFlow;
|
|
252
545
|
}
|
|
253
546
|
export function redacted(value) {
|
|
@@ -303,7 +596,7 @@ export async function resolveApiKey(auth, env = process.env) {
|
|
|
303
596
|
return env.KIRO_API_KEY;
|
|
304
597
|
try {
|
|
305
598
|
const credential = await auth();
|
|
306
|
-
return credential.
|
|
599
|
+
return credential.key || credential.access || "";
|
|
307
600
|
}
|
|
308
601
|
catch {
|
|
309
602
|
return "";
|
package/dist/config.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { KiroCliLoginOptions } from "./auth.js";
|
|
1
2
|
export type BackendMode = "auto" | "fetch" | "cli-chat" | "acp";
|
|
2
3
|
export type ModelDiscoveryMode = "auto" | "off";
|
|
3
4
|
export interface KiroPluginOptions {
|
|
4
5
|
readonly providerID: string;
|
|
5
6
|
readonly region: string;
|
|
7
|
+
readonly login: KiroCliLoginOptions;
|
|
6
8
|
readonly endpoint?: string;
|
|
7
9
|
readonly backend: BackendMode;
|
|
8
10
|
readonly modelDiscovery: ModelDiscoveryMode;
|
package/dist/config.js
CHANGED
|
@@ -43,6 +43,20 @@ function optionalString(value) {
|
|
|
43
43
|
const trimmed = value.trim();
|
|
44
44
|
return trimmed || undefined;
|
|
45
45
|
}
|
|
46
|
+
function loginOptions(value) {
|
|
47
|
+
const input = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
48
|
+
const license = input.license === "free" || input.license === "pro" ? input.license : undefined;
|
|
49
|
+
const identityProvider = optionalString(input.identityProvider);
|
|
50
|
+
const region = optionalString(input.region);
|
|
51
|
+
const extraArgs = stringArray(input.extraArgs);
|
|
52
|
+
return {
|
|
53
|
+
...(license ? { license } : {}),
|
|
54
|
+
...(identityProvider ? { identityProvider } : {}),
|
|
55
|
+
...(region ? { region } : {}),
|
|
56
|
+
useDeviceFlow: input.useDeviceFlow === true,
|
|
57
|
+
...(extraArgs.length > 0 ? { extraArgs } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
46
60
|
export function loadOptions(raw = {}) {
|
|
47
61
|
const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
48
62
|
const backend = typeof input.backend === "string" && BACKENDS.has(input.backend) ? input.backend : "auto";
|
|
@@ -58,6 +72,7 @@ export function loadOptions(raw = {}) {
|
|
|
58
72
|
return {
|
|
59
73
|
providerID: typeof input.providerID === "string" && input.providerID ? input.providerID : DEFAULT_PROVIDER_ID,
|
|
60
74
|
region: typeof input.region === "string" && input.region ? input.region : DEFAULT_REGION,
|
|
75
|
+
login: loginOptions(input.login),
|
|
61
76
|
...(endpoint ? { endpoint } : {}),
|
|
62
77
|
backend,
|
|
63
78
|
modelDiscovery,
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { AcpConnection, AcpNotificationHandler, AcpStdioClientOptions, Json
|
|
|
3
3
|
export { acpPermissionResponse, KiroAcpTransport } from "./acp-transport.js";
|
|
4
4
|
export type { AcpSessionClient, KiroAcpTransportOptions } from "./acp-transport.js";
|
|
5
5
|
export { createKiroPlugin, effectiveBackend, KiroPlugin } from "./plugin.js";
|
|
6
|
-
export { detectAuth, extractKiroLoginUrl, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, resolveApiKey, runKiroLoginFlowOnce, startKiroCliLogin, startKiroCliLoginOnce, } from "./auth.js";
|
|
6
|
+
export { authorizeKiroDevice, credentialFromKiroDeviceAuthKey, decodeKiroDeviceAuthKey, detectAuth, encodeKiroDeviceAuthKey, extractKiroLoginUrl, isKiroDeviceAuthKey, kiroDeviceVerificationUrl, pollKiroDeviceToken, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, refreshKiroDeviceAuthCredential, resolveApiKey, runKiroLoginFlowOnce, startKiroCliLogin, startKiroCliLoginOnce, } from "./auth.js";
|
|
7
7
|
export { cliChatArgs, KiroCliChatTransport, promptForCli } from "./cli-transport.js";
|
|
8
8
|
export { createKiroFetch } from "./fetch-adapter.js";
|
|
9
9
|
export { loadOptions } from "./config.js";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { KiroPlugin } from "./plugin.js";
|
|
|
2
2
|
export { AcpJsonRpcClient, createAcpStdioClient, decodeJsonRpc, encodeJsonRpc } from "./acp-client.js";
|
|
3
3
|
export { acpPermissionResponse, KiroAcpTransport } from "./acp-transport.js";
|
|
4
4
|
export { createKiroPlugin, effectiveBackend, KiroPlugin } from "./plugin.js";
|
|
5
|
-
export { detectAuth, extractKiroLoginUrl, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, resolveApiKey, runKiroLoginFlowOnce, startKiroCliLogin, startKiroCliLoginOnce, } from "./auth.js";
|
|
5
|
+
export { authorizeKiroDevice, credentialFromKiroDeviceAuthKey, decodeKiroDeviceAuthKey, detectAuth, encodeKiroDeviceAuthKey, extractKiroLoginUrl, isKiroDeviceAuthKey, kiroDeviceVerificationUrl, pollKiroDeviceToken, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, refreshKiroDeviceAuthCredential, resolveApiKey, runKiroLoginFlowOnce, startKiroCliLogin, startKiroCliLoginOnce, } from "./auth.js";
|
|
6
6
|
export { cliChatArgs, KiroCliChatTransport, promptForCli } from "./cli-transport.js";
|
|
7
7
|
export { createKiroFetch } from "./fetch-adapter.js";
|
|
8
8
|
export { loadOptions } from "./config.js";
|
package/dist/plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
2
|
import { KiroAcpTransport } from "./acp-transport.js";
|
|
3
|
-
import { detectAuth, resolveApiKey,
|
|
3
|
+
import { authorizeKiroDevice, credentialFromKiroDeviceAuthKey, detectAuth, encodeKiroDeviceAuthKey, isKiroDeviceAuthKey, kiroDeviceVerificationUrl, pollKiroDeviceToken, resolveApiKey, runKiroLoginFlowOnce, } from "./auth.js";
|
|
4
4
|
import { KiroCliChatTransport } from "./cli-transport.js";
|
|
5
5
|
import { loadOptions } from "./config.js";
|
|
6
6
|
import { createKiroFetch } from "./fetch-adapter.js";
|
|
@@ -53,6 +53,10 @@ function bearerToken(init) {
|
|
|
53
53
|
const match = header?.match(/^Bearer\s+(.+)$/i);
|
|
54
54
|
return match?.[1] || undefined;
|
|
55
55
|
}
|
|
56
|
+
function inputString(inputs, key) {
|
|
57
|
+
const value = inputs?.[key];
|
|
58
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
59
|
+
}
|
|
56
60
|
export function effectiveBackend(options, accessToken) {
|
|
57
61
|
const apiKey = accessToken || process.env.KIRO_API_KEY;
|
|
58
62
|
if (options.backend === "acp")
|
|
@@ -73,11 +77,20 @@ function localTransport(options, accessToken) {
|
|
|
73
77
|
return new KiroCliChatTransport({
|
|
74
78
|
trustAllTools: options.trustAllTools,
|
|
75
79
|
...(options.requestTimeoutMs ? { requestTimeoutMs: options.requestTimeoutMs } : {}),
|
|
80
|
+
login: () => runKiroLoginFlowOnce({ login: options.login }),
|
|
76
81
|
});
|
|
77
82
|
}
|
|
78
83
|
if (backend === "fetch") {
|
|
79
84
|
const apiKey = accessToken || process.env.KIRO_API_KEY;
|
|
80
|
-
|
|
85
|
+
if (isKiroDeviceAuthKey(apiKey)) {
|
|
86
|
+
return new KiroRestTransport(fetchTransportOptions(options), {
|
|
87
|
+
credentialProvider: () => credentialFromKiroDeviceAuthKey(apiKey).catch(() => undefined),
|
|
88
|
+
login: async () => false,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return new KiroRestTransport(fetchTransportOptions(options, apiKey === "kiro-plugin-local-transport" ? undefined : apiKey), {
|
|
92
|
+
login: () => runKiroLoginFlowOnce({ login: options.login }),
|
|
93
|
+
});
|
|
81
94
|
}
|
|
82
95
|
return undefined;
|
|
83
96
|
}
|
|
@@ -119,7 +132,7 @@ export function createKiroPlugin() {
|
|
|
119
132
|
return;
|
|
120
133
|
}
|
|
121
134
|
discoveryAttempted = true;
|
|
122
|
-
discovery ??= refreshModelCacheFromCommand(modelCache, options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1)
|
|
135
|
+
discovery ??= refreshModelCacheFromCommand(modelCache, options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1))
|
|
123
136
|
.catch(() => undefined)
|
|
124
137
|
.then(() => {
|
|
125
138
|
discovery = undefined;
|
|
@@ -173,26 +186,66 @@ export function createKiroPlugin() {
|
|
|
173
186
|
methods: [
|
|
174
187
|
{
|
|
175
188
|
type: "oauth",
|
|
176
|
-
label: "Kiro
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
189
|
+
label: "Kiro device login",
|
|
190
|
+
prompts: [
|
|
191
|
+
{
|
|
192
|
+
type: "text",
|
|
193
|
+
key: "startUrl",
|
|
194
|
+
message: options.login.identityProvider
|
|
195
|
+
? `IAM Identity Center Start URL (current: ${options.login.identityProvider}, leave blank to keep)`
|
|
196
|
+
: "IAM Identity Center Start URL (leave blank for AWS Builder ID)",
|
|
197
|
+
placeholder: "https://your-company.awsapps.com/start",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
type: "text",
|
|
201
|
+
key: "idcRegion",
|
|
202
|
+
message: options.login.region && options.login.region !== "us-east-1"
|
|
203
|
+
? `IAM Identity Center region (current: ${options.login.region}, leave blank to keep)`
|
|
204
|
+
: "IAM Identity Center region (leave blank for us-east-1)",
|
|
205
|
+
placeholder: "us-east-1",
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
type: "text",
|
|
209
|
+
key: "profileArn",
|
|
210
|
+
message: options.profileArn
|
|
211
|
+
? `Profile ARN (current: ${options.profileArn}, leave blank to keep)`
|
|
212
|
+
: "Profile ARN (optional, improves region/profile routing for IAM Identity Center)",
|
|
213
|
+
placeholder: "arn:aws:codewhisperer:us-east-1:123456789012:profile/XXXXXXXXXX",
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
authorize: async (inputs) => {
|
|
217
|
+
const promptInputs = inputs;
|
|
218
|
+
const startUrl = inputString(promptInputs, "startUrl") ?? options.login.identityProvider;
|
|
219
|
+
const idcRegion = inputString(promptInputs, "idcRegion") ?? options.login.region ?? "us-east-1";
|
|
220
|
+
const profileArn = inputString(promptInputs, "profileArn") ?? options.profileArn;
|
|
221
|
+
const authorization = await authorizeKiroDevice({
|
|
222
|
+
region: idcRegion,
|
|
223
|
+
...(startUrl ? { identityProvider: startUrl } : {}),
|
|
224
|
+
});
|
|
225
|
+
const url = startUrl
|
|
226
|
+
? kiroDeviceVerificationUrl(authorization.startUrl, authorization.userCode)
|
|
227
|
+
: authorization.verificationUrlComplete;
|
|
180
228
|
return {
|
|
181
|
-
url
|
|
182
|
-
instructions:
|
|
229
|
+
url,
|
|
230
|
+
instructions: `Open the verification URL and complete Kiro sign-in.\nCode: ${authorization.userCode}`,
|
|
183
231
|
method: "auto",
|
|
184
232
|
callback: async () => {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
233
|
+
const credential = await pollKiroDeviceToken(authorization, {
|
|
234
|
+
region: options.region,
|
|
235
|
+
...(profileArn ? { profileArn } : {}),
|
|
236
|
+
});
|
|
237
|
+
const key = encodeKiroDeviceAuthKey(credential);
|
|
189
238
|
return {
|
|
190
239
|
type: "success",
|
|
191
|
-
key
|
|
240
|
+
key,
|
|
241
|
+
access: key,
|
|
242
|
+
refresh: credential.refreshToken,
|
|
243
|
+
expires: credential.expiresAt,
|
|
192
244
|
metadata: {
|
|
193
|
-
source: "kiro-
|
|
194
|
-
|
|
195
|
-
|
|
245
|
+
source: "kiro-device-auth",
|
|
246
|
+
region: credential.region,
|
|
247
|
+
oidcRegion: credential.oidcRegion,
|
|
248
|
+
...(credential.profileArn ? { profileArn: credential.profileArn } : {}),
|
|
196
249
|
},
|
|
197
250
|
};
|
|
198
251
|
},
|