@kora-platform/cli 0.8.0-rc3 → 0.8.0-rc7
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/dist/api-client.d.ts +32 -13
- package/dist/api-client.js +28 -18
- package/dist/api-types.d.ts +22 -2
- package/dist/artifact-commands.js +5 -3
- package/dist/auth-commands.js +84 -8
- package/dist/cli-errors.d.ts +7 -1
- package/dist/cli-errors.js +12 -1
- package/dist/command-flags.d.ts +1 -0
- package/dist/command-flags.js +7 -0
- package/dist/command-registry.js +62 -39
- package/dist/commands.js +89 -28
- package/dist/error-code.d.ts +2 -0
- package/dist/error-code.js +9 -0
- package/dist/extension-commands.js +2 -2
- package/dist/files.d.ts +12 -1
- package/dist/files.js +102 -16
- package/dist/format.d.ts +1 -0
- package/dist/format.js +11 -6
- package/dist/runner.js +14 -5
- package/dist/session-store.js +80 -0
- package/dist/transport-refresh.d.ts +10 -0
- package/dist/transport-refresh.js +51 -0
- package/dist/transport.d.ts +21 -0
- package/dist/transport.js +80 -36
- package/package.json +1 -1
package/dist/transport.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { normalizePublicErrorCode, readPublicErrorCodeFromType } from "./error-code.js";
|
|
1
2
|
import { extractRefreshTokenFromHeaders } from "./session.js";
|
|
3
|
+
import { refreshSessionWithCoordination, resolveEffectiveSession } from "./transport-refresh.js";
|
|
2
4
|
export class ApiError extends Error {
|
|
5
|
+
code;
|
|
3
6
|
detail;
|
|
4
7
|
details;
|
|
5
8
|
instance;
|
|
@@ -10,6 +13,7 @@ export class ApiError extends Error {
|
|
|
10
13
|
constructor(input) {
|
|
11
14
|
super(input.detail);
|
|
12
15
|
this.name = "ApiError";
|
|
16
|
+
this.code = normalizePublicErrorCode(input.code ?? readPublicErrorCodeFromType(input.type));
|
|
13
17
|
this.detail = input.detail;
|
|
14
18
|
if (input.details) {
|
|
15
19
|
this.details = input.details;
|
|
@@ -25,6 +29,13 @@ export class ApiError extends Error {
|
|
|
25
29
|
}
|
|
26
30
|
export function createPlatformTransport(input) {
|
|
27
31
|
const now = input.now ?? Date.now;
|
|
32
|
+
const refreshSession = async (session) => refreshSessionWithCoordination({
|
|
33
|
+
isRefreshTokenInvalidError,
|
|
34
|
+
refreshSessionDirect: async (effectiveSession) => refreshSessionDirect(input.sessionStore, effectiveSession),
|
|
35
|
+
session,
|
|
36
|
+
sessionStore: input.sessionStore,
|
|
37
|
+
shouldRefresh: (effectiveSession) => shouldRefresh(effectiveSession, now)
|
|
38
|
+
});
|
|
28
39
|
return {
|
|
29
40
|
async getAuthSettings(baseUrl) {
|
|
30
41
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
@@ -66,19 +77,54 @@ export function createPlatformTransport(input) {
|
|
|
66
77
|
await input.sessionStore.write(session);
|
|
67
78
|
return session;
|
|
68
79
|
},
|
|
69
|
-
async
|
|
70
|
-
const
|
|
80
|
+
async startDeviceLogin(baseUrl) {
|
|
81
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
82
|
+
const path = "/api/v1/auth/device/start";
|
|
83
|
+
const response = await fetch(joinBaseUrl(normalizedBaseUrl, path), {
|
|
84
|
+
method: "POST"
|
|
85
|
+
});
|
|
86
|
+
return handleJsonResponse(response, path);
|
|
87
|
+
},
|
|
88
|
+
async claimDeviceLogin(claim) {
|
|
89
|
+
const normalizedBaseUrl = normalizeBaseUrl(claim.baseUrl);
|
|
90
|
+
const path = "/api/v1/auth/device/claim";
|
|
91
|
+
const response = await fetch(joinBaseUrl(normalizedBaseUrl, path), {
|
|
71
92
|
body: JSON.stringify({
|
|
72
|
-
|
|
93
|
+
deviceCode: claim.deviceCode
|
|
73
94
|
}),
|
|
74
95
|
headers: {
|
|
75
96
|
"content-type": "application/json"
|
|
76
97
|
},
|
|
77
98
|
method: "POST"
|
|
78
99
|
});
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw await toApiError(response, path);
|
|
102
|
+
}
|
|
103
|
+
const payload = await response.json();
|
|
104
|
+
if (payload.status !== "approved") {
|
|
105
|
+
return { status: payload.status };
|
|
106
|
+
}
|
|
107
|
+
if (!payload.tokens || !payload.user) {
|
|
108
|
+
throw new Error("Device login approval response did not include a session.");
|
|
109
|
+
}
|
|
110
|
+
const refreshToken = extractRefreshTokenFromHeaders(response.headers);
|
|
111
|
+
if (!refreshToken) {
|
|
112
|
+
throw new Error("Refresh token cookie was not returned by the Platform API.");
|
|
113
|
+
}
|
|
114
|
+
const session = {
|
|
115
|
+
accessToken: payload.tokens.accessToken,
|
|
116
|
+
accessTokenPayload: payload.tokens.accessTokenPayload,
|
|
117
|
+
activeOrg: null,
|
|
118
|
+
baseUrl: normalizedBaseUrl,
|
|
119
|
+
refreshToken,
|
|
120
|
+
refreshTokenExpiresAt: payload.tokens.refreshTokenExpiresAt,
|
|
121
|
+
user: payload.user
|
|
122
|
+
};
|
|
123
|
+
await input.sessionStore.write(session);
|
|
124
|
+
return { session, status: "approved" };
|
|
125
|
+
},
|
|
126
|
+
async refreshSession(session) {
|
|
127
|
+
return refreshSession(session);
|
|
82
128
|
},
|
|
83
129
|
async requestJson(request) {
|
|
84
130
|
let session = await resolveEffectiveSession(request.session, input.sessionStore);
|
|
@@ -92,11 +138,11 @@ export function createPlatformTransport(input) {
|
|
|
92
138
|
});
|
|
93
139
|
}
|
|
94
140
|
if (shouldRefresh(session, now) && canRefresh(session)) {
|
|
95
|
-
session = await
|
|
141
|
+
session = await refreshSession(session);
|
|
96
142
|
}
|
|
97
143
|
const firstResponse = await authenticatedFetch(session, request);
|
|
98
144
|
if (firstResponse.status === 401 && canRefresh(session)) {
|
|
99
|
-
session = await
|
|
145
|
+
session = await refreshSession(session);
|
|
100
146
|
return handleJsonResponse(await authenticatedFetch(session, request), request.path);
|
|
101
147
|
}
|
|
102
148
|
return handleJsonResponse(firstResponse, request.path);
|
|
@@ -113,17 +159,36 @@ export function createPlatformTransport(input) {
|
|
|
113
159
|
});
|
|
114
160
|
}
|
|
115
161
|
if (shouldRefresh(session, now) && canRefresh(session)) {
|
|
116
|
-
session = await
|
|
162
|
+
session = await refreshSession(session);
|
|
117
163
|
}
|
|
118
164
|
const firstResponse = await authenticatedFetch(session, request);
|
|
119
165
|
if (firstResponse.status === 401 && canRefresh(session)) {
|
|
120
|
-
session = await
|
|
166
|
+
session = await refreshSession(session);
|
|
121
167
|
return handleBytesResponse(await authenticatedFetch(session, request), request);
|
|
122
168
|
}
|
|
123
169
|
return handleBytesResponse(firstResponse, request);
|
|
124
170
|
}
|
|
125
171
|
};
|
|
126
172
|
}
|
|
173
|
+
async function refreshSessionDirect(sessionStore, session) {
|
|
174
|
+
const response = await fetch(joinBaseUrl(session.baseUrl, "/api/v1/auth/refresh"), {
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
refreshToken: session.refreshToken
|
|
177
|
+
}),
|
|
178
|
+
headers: {
|
|
179
|
+
"content-type": "application/json"
|
|
180
|
+
},
|
|
181
|
+
method: "POST"
|
|
182
|
+
});
|
|
183
|
+
const nextSession = await parseSessionResponse(response, session.baseUrl, "/api/v1/auth/refresh", session.activeOrg);
|
|
184
|
+
await sessionStore.write(nextSession);
|
|
185
|
+
return nextSession;
|
|
186
|
+
}
|
|
187
|
+
function isRefreshTokenInvalidError(error) {
|
|
188
|
+
return error instanceof ApiError &&
|
|
189
|
+
error.code === "auth/token_invalid" &&
|
|
190
|
+
error.instance === "/api/v1/auth/refresh";
|
|
191
|
+
}
|
|
127
192
|
async function authenticatedFetch(session, request) {
|
|
128
193
|
const body = resolveRequestBody(request.body, session);
|
|
129
194
|
const hasJsonBody = body !== undefined;
|
|
@@ -243,11 +308,12 @@ async function readResponseBytes(response, request) {
|
|
|
243
308
|
}
|
|
244
309
|
function createMaxBytesError(request, actualBytes, maxBytes) {
|
|
245
310
|
return request.createMaxBytesError?.({ actualBytes, maxBytes }) ?? new ApiError({
|
|
311
|
+
code: "request/too_large",
|
|
246
312
|
detail: `Response body is ${String(actualBytes)} bytes, which exceeds the limit of ${String(maxBytes)} bytes.`,
|
|
247
313
|
instance: request.path,
|
|
248
314
|
status: 413,
|
|
249
315
|
title: "Payload Too Large",
|
|
250
|
-
type: "https://errors.kora.dev/request/
|
|
316
|
+
type: "https://errors.kora.dev/request/too_large"
|
|
251
317
|
});
|
|
252
318
|
}
|
|
253
319
|
function concatBytes(chunks, totalBytes) {
|
|
@@ -271,10 +337,11 @@ async function toApiError(response, path) {
|
|
|
271
337
|
if (contentType.includes("application/json")) {
|
|
272
338
|
const rawBody = await response.text();
|
|
273
339
|
const parsed = tryParseJsonErrorBody(rawBody);
|
|
274
|
-
const code = parsed?.error?.code ?? "internal/error";
|
|
340
|
+
const code = normalizePublicErrorCode(parsed?.error?.code ?? "internal/error");
|
|
275
341
|
const rawDetail = (parsed?.error?.message ?? rawBody) || response.statusText;
|
|
276
342
|
const detail = relabelBackendText(rawDetail || response.statusText);
|
|
277
343
|
return new ApiError({
|
|
344
|
+
code,
|
|
278
345
|
detail,
|
|
279
346
|
...(parsed?.error?.details ? { details: parsed.error.details } : {}),
|
|
280
347
|
instance: path,
|
|
@@ -286,6 +353,7 @@ async function toApiError(response, path) {
|
|
|
286
353
|
}
|
|
287
354
|
const rawDetail = await response.text();
|
|
288
355
|
return new ApiError({
|
|
356
|
+
code: "internal/error",
|
|
289
357
|
detail: relabelBackendText(rawDetail),
|
|
290
358
|
instance: path,
|
|
291
359
|
...(rawDetail ? { rawDetail } : {}),
|
|
@@ -337,27 +405,3 @@ function shouldRefresh(session, now) {
|
|
|
337
405
|
function canRefresh(session) {
|
|
338
406
|
return session.refreshToken.trim().length > 0;
|
|
339
407
|
}
|
|
340
|
-
async function resolveEffectiveSession(requestSession, sessionStore) {
|
|
341
|
-
if (!requestSession) {
|
|
342
|
-
return null;
|
|
343
|
-
}
|
|
344
|
-
const storedSession = await sessionStore.read();
|
|
345
|
-
if (!storedSession || !isSameSessionIdentity(storedSession, requestSession)) {
|
|
346
|
-
return requestSession;
|
|
347
|
-
}
|
|
348
|
-
return isSessionNewer(storedSession, requestSession) ? storedSession : requestSession;
|
|
349
|
-
}
|
|
350
|
-
function isSameSessionIdentity(candidate, current) {
|
|
351
|
-
return candidate.baseUrl === current.baseUrl
|
|
352
|
-
&& candidate.user.id === current.user.id
|
|
353
|
-
&& candidate.accessTokenPayload.sub === current.accessTokenPayload.sub;
|
|
354
|
-
}
|
|
355
|
-
function isSessionNewer(candidate, current) {
|
|
356
|
-
if (candidate.accessToken === current.accessToken && candidate.refreshToken === current.refreshToken) {
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
if (candidate.accessTokenPayload.exp !== current.accessTokenPayload.exp) {
|
|
360
|
-
return candidate.accessTokenPayload.exp > current.accessTokenPayload.exp;
|
|
361
|
-
}
|
|
362
|
-
return candidate.accessTokenPayload.iat >= current.accessTokenPayload.iat;
|
|
363
|
-
}
|