@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/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 refreshSession(session) {
70
- const response = await fetch(joinBaseUrl(session.baseUrl, "/api/v1/auth/refresh"), {
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
- refreshToken: session.refreshToken
93
+ deviceCode: claim.deviceCode
73
94
  }),
74
95
  headers: {
75
96
  "content-type": "application/json"
76
97
  },
77
98
  method: "POST"
78
99
  });
79
- const nextSession = await parseSessionResponse(response, session.baseUrl, "/api/v1/auth/refresh", session.activeOrg);
80
- await input.sessionStore.write(nextSession);
81
- return nextSession;
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 this.refreshSession(session);
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 this.refreshSession(session);
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 this.refreshSession(session);
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 this.refreshSession(session);
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/too-large"
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
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kora-platform/cli",
3
- "version": "0.8.0-rc3",
3
+ "version": "0.8.0-rc7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/library.js",