@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/files.js
CHANGED
|
@@ -7,14 +7,38 @@ const MAX_IMPORT_FILE_COUNT = 500;
|
|
|
7
7
|
const MAX_IMPORT_FILE_BYTES = 1_000_000;
|
|
8
8
|
const MAX_PACKAGE_FILE_BYTES = 2_000_000;
|
|
9
9
|
const MAX_IMPORT_TOTAL_BYTES = 10_000_000;
|
|
10
|
-
export async function readJsonInputSpecifier(specifier, stdin, instance) {
|
|
10
|
+
export async function readJsonInputSpecifier(specifier, stdin, instance, options = {}) {
|
|
11
11
|
if (specifier === "-") {
|
|
12
|
-
return readJsonObject(await readStream(stdin), instance
|
|
12
|
+
return readJsonObject(await readStream(stdin), instance, {
|
|
13
|
+
source: "stdin",
|
|
14
|
+
...options
|
|
15
|
+
});
|
|
13
16
|
}
|
|
14
17
|
if (!specifier.startsWith("@")) {
|
|
15
|
-
throw usageProblem("Structured JSON input must use @file.json or - for stdin.", instance
|
|
18
|
+
throw usageProblem("Structured JSON input must use @file.json or - for stdin.", instance, {
|
|
19
|
+
...(options.flag ? { flag: options.flag } : {})
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const filePath = resolve(specifier.slice(1));
|
|
23
|
+
let source;
|
|
24
|
+
try {
|
|
25
|
+
source = await readFile(filePath, "utf8");
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const nativeCode = readNodeErrorCode(error);
|
|
29
|
+
if (nativeCode && isLocalPathReadErrorCode(nativeCode)) {
|
|
30
|
+
throw usageProblem(`Structured JSON input file ${filePath} could not be read: ${nativeCode}.`, instance, {
|
|
31
|
+
filePath,
|
|
32
|
+
...(options.flag ? { flag: options.flag } : {}),
|
|
33
|
+
nativeCode
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
16
37
|
}
|
|
17
|
-
return readJsonObject(
|
|
38
|
+
return readJsonObject(source, instance, {
|
|
39
|
+
filePath,
|
|
40
|
+
...options
|
|
41
|
+
});
|
|
18
42
|
}
|
|
19
43
|
export async function readTextInputSpecifier(specifier, stdin, instance) {
|
|
20
44
|
if (specifier === "-") {
|
|
@@ -35,12 +59,26 @@ export function isZipArchivePath(pathValue) {
|
|
|
35
59
|
return extname(pathValue).toLowerCase() === ".zip";
|
|
36
60
|
}
|
|
37
61
|
export async function readArchiveBytes(pathValue, instance) {
|
|
62
|
+
const file = await readLocalFileBytes(pathValue, instance, {
|
|
63
|
+
regularFileMessage: "Archive path must be a regular .zip file, not a directory or symbolic link."
|
|
64
|
+
});
|
|
65
|
+
return file.bytes;
|
|
66
|
+
}
|
|
67
|
+
export async function readLocalFileBytes(pathValue, instance, options = {}) {
|
|
38
68
|
const absolutePath = resolve(pathValue);
|
|
39
|
-
const pathStat = await
|
|
69
|
+
const pathStat = await readLocalPathStat(absolutePath, instance);
|
|
40
70
|
if (pathStat.isSymbolicLink() || !pathStat.isFile()) {
|
|
41
|
-
throw usageProblem("
|
|
71
|
+
throw usageProblem(options.regularFileMessage ?? "Path must be a regular file, not a directory or symbolic link.", instance);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return {
|
|
75
|
+
absolutePath,
|
|
76
|
+
bytes: await readFile(absolutePath)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
throwLocalPathReadProblem(error, absolutePath, instance);
|
|
42
81
|
}
|
|
43
|
-
return await readFile(absolutePath);
|
|
44
82
|
}
|
|
45
83
|
export async function readWorkspaceTestEntries(pathValue) {
|
|
46
84
|
return await readUtf8Entries(pathValue, {
|
|
@@ -50,7 +88,7 @@ export async function readWorkspaceTestEntries(pathValue) {
|
|
|
50
88
|
}
|
|
51
89
|
async function readUtf8Entries(pathValue, options) {
|
|
52
90
|
const absolutePath = resolve(pathValue);
|
|
53
|
-
const pathStat = await
|
|
91
|
+
const pathStat = await readLocalPathStat(absolutePath, options.instance);
|
|
54
92
|
if (pathStat.isSymbolicLink()) {
|
|
55
93
|
throw usageProblem("Import path must be a regular file or directory, not a symbolic link.", options.instance);
|
|
56
94
|
}
|
|
@@ -151,7 +189,7 @@ export async function writePackageExport(outPath, envelope) {
|
|
|
151
189
|
}
|
|
152
190
|
export async function readPackageFileEntries(pathValue, instance) {
|
|
153
191
|
const absolutePath = resolve(pathValue);
|
|
154
|
-
const pathStat = await
|
|
192
|
+
const pathStat = await readLocalPathStat(absolutePath, instance);
|
|
155
193
|
if (pathStat.isSymbolicLink()) {
|
|
156
194
|
throw usageProblem("Package path must be a regular file or directory, not a symbolic link.", instance);
|
|
157
195
|
}
|
|
@@ -161,7 +199,9 @@ export async function readPackageFileEntries(pathValue, instance) {
|
|
|
161
199
|
totalBytes: 0
|
|
162
200
|
}, instance);
|
|
163
201
|
return [{
|
|
164
|
-
contentBase64: (await
|
|
202
|
+
contentBase64: (await readLocalFileBytes(absolutePath, instance, {
|
|
203
|
+
regularFileMessage: "Package path must be a regular file or directory, not a symbolic link."
|
|
204
|
+
})).bytes.toString("base64"),
|
|
165
205
|
path: basename(absolutePath)
|
|
166
206
|
}];
|
|
167
207
|
}
|
|
@@ -173,16 +213,24 @@ export async function readPackageFileEntries(pathValue, instance) {
|
|
|
173
213
|
entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
174
214
|
return entries;
|
|
175
215
|
}
|
|
176
|
-
function readJsonObject(source, instance) {
|
|
216
|
+
function readJsonObject(source, instance, options) {
|
|
177
217
|
let parsed;
|
|
178
218
|
try {
|
|
179
219
|
parsed = JSON.parse(source);
|
|
180
220
|
}
|
|
181
221
|
catch {
|
|
182
|
-
|
|
222
|
+
const sourceLabel = "filePath" in options ? `file ${options.filePath}` : "from stdin";
|
|
223
|
+
throw usageProblem(`Structured JSON input ${sourceLabel} must be valid JSON.`, instance, {
|
|
224
|
+
...("filePath" in options ? { filePath: options.filePath } : { source: options.source }),
|
|
225
|
+
...(options.flag ? { flag: options.flag } : {})
|
|
226
|
+
});
|
|
183
227
|
}
|
|
184
228
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
185
|
-
|
|
229
|
+
const sourceLabel = "filePath" in options ? `file ${options.filePath}` : "from stdin";
|
|
230
|
+
throw usageProblem(`Structured JSON input ${sourceLabel} must be a JSON object.`, instance, {
|
|
231
|
+
...("filePath" in options ? { filePath: options.filePath } : { source: options.source }),
|
|
232
|
+
...(options.flag ? { flag: options.flag } : {})
|
|
233
|
+
});
|
|
186
234
|
}
|
|
187
235
|
return parsed;
|
|
188
236
|
}
|
|
@@ -232,10 +280,10 @@ async function walkPackageDirectory(root, current, entries, counters, instance)
|
|
|
232
280
|
if (!child.isFile()) {
|
|
233
281
|
continue;
|
|
234
282
|
}
|
|
235
|
-
const childStat = await
|
|
283
|
+
const childStat = await readLocalPathStat(childPath, instance);
|
|
236
284
|
assertPackageFileCanBeRead(childStat.size, relativePath, counters, instance);
|
|
237
285
|
entries.push({
|
|
238
|
-
contentBase64: (await
|
|
286
|
+
contentBase64: (await readLocalFileBytes(childPath, instance)).bytes.toString("base64"),
|
|
239
287
|
path: relativePath
|
|
240
288
|
});
|
|
241
289
|
}
|
|
@@ -289,7 +337,13 @@ async function readLimitedUtf8File(filePath, displayPath, counters, options) {
|
|
|
289
337
|
if (counters.fileCount >= MAX_IMPORT_FILE_COUNT) {
|
|
290
338
|
throw usageProblem(`Import has more than ${String(MAX_IMPORT_FILE_COUNT)} files.`, options.instance);
|
|
291
339
|
}
|
|
292
|
-
|
|
340
|
+
let content;
|
|
341
|
+
try {
|
|
342
|
+
content = await readFile(filePath, "utf8");
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
throwLocalPathReadProblem(error, filePath, options.instance);
|
|
346
|
+
}
|
|
293
347
|
const bytes = Buffer.byteLength(content, "utf8");
|
|
294
348
|
if (bytes > options.maxFileBytes) {
|
|
295
349
|
throw usageProblem(`Import file ${displayPath} exceeds the per-file byte limit.`, options.instance);
|
|
@@ -320,3 +374,35 @@ function isNodeErrorWithCode(error, code) {
|
|
|
320
374
|
"code" in error &&
|
|
321
375
|
error.code === code;
|
|
322
376
|
}
|
|
377
|
+
function readNodeErrorCode(error) {
|
|
378
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
const code = error.code;
|
|
382
|
+
return typeof code === "string" && code.length > 0 ? code : undefined;
|
|
383
|
+
}
|
|
384
|
+
function isLocalPathReadErrorCode(code) {
|
|
385
|
+
return code === "EACCES" ||
|
|
386
|
+
code === "EISDIR" ||
|
|
387
|
+
code === "ENOENT" ||
|
|
388
|
+
code === "ENOTDIR" ||
|
|
389
|
+
code === "EPERM";
|
|
390
|
+
}
|
|
391
|
+
async function readLocalPathStat(absolutePath, instance) {
|
|
392
|
+
try {
|
|
393
|
+
return await lstat(absolutePath);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
throwLocalPathReadProblem(error, absolutePath, instance);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function throwLocalPathReadProblem(error, absolutePath, instance) {
|
|
400
|
+
const nativeCode = readNodeErrorCode(error);
|
|
401
|
+
if (nativeCode && isLocalPathReadErrorCode(nativeCode)) {
|
|
402
|
+
throw usageProblem(`Local path ${absolutePath} could not be read: ${nativeCode}.`, instance, {
|
|
403
|
+
nativeCode,
|
|
404
|
+
path: absolutePath
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
package/dist/format.d.ts
CHANGED
package/dist/format.js
CHANGED
|
@@ -7,12 +7,17 @@ export function renderJsonEnvelope(input) {
|
|
|
7
7
|
}
|
|
8
8
|
export function renderProblemJson(input) {
|
|
9
9
|
return `${JSON.stringify({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
error: {
|
|
11
|
+
code: input.code,
|
|
12
|
+
details: {
|
|
13
|
+
...(input.details ?? {}),
|
|
14
|
+
instance: input.instance,
|
|
15
|
+
status: input.status,
|
|
16
|
+
title: input.title,
|
|
17
|
+
type: input.type
|
|
18
|
+
},
|
|
19
|
+
message: input.detail
|
|
20
|
+
}
|
|
16
21
|
}, null, 2)}\n`;
|
|
17
22
|
}
|
|
18
23
|
export function renderTable(rows, columns) {
|
package/dist/runner.js
CHANGED
|
@@ -89,6 +89,7 @@ export async function runCli(argv, input = {}) {
|
|
|
89
89
|
: `${problem.title}: ${formatProblemDetail(problem.detail, details)}`}\n`,
|
|
90
90
|
stdout: wantsJson
|
|
91
91
|
? renderProblemJson({
|
|
92
|
+
code: problem.code,
|
|
92
93
|
detail: problem.detail,
|
|
93
94
|
...(details ? { details } : {}),
|
|
94
95
|
instance: problem.instance,
|
|
@@ -231,7 +232,10 @@ function parseInvocation(tokens, commandFilter) {
|
|
|
231
232
|
positionals.push(token);
|
|
232
233
|
continue;
|
|
233
234
|
}
|
|
234
|
-
const
|
|
235
|
+
const rawFlag = token.slice(2);
|
|
236
|
+
const valueSeparatorIndex = rawFlag.indexOf("=");
|
|
237
|
+
const rawName = valueSeparatorIndex === -1 ? rawFlag : rawFlag.slice(0, valueSeparatorIndex);
|
|
238
|
+
const maybeValue = valueSeparatorIndex === -1 ? undefined : rawFlag.slice(valueSeparatorIndex + 1);
|
|
235
239
|
const flag = flagDefinitions.get(rawName);
|
|
236
240
|
if (!flag) {
|
|
237
241
|
const detail = rawName === "output"
|
|
@@ -246,11 +250,16 @@ function parseInvocation(tokens, commandFilter) {
|
|
|
246
250
|
flags[rawName] = true;
|
|
247
251
|
continue;
|
|
248
252
|
}
|
|
249
|
-
|
|
250
|
-
if (
|
|
251
|
-
|
|
253
|
+
let valueSource;
|
|
254
|
+
if (maybeValue !== undefined) {
|
|
255
|
+
valueSource = maybeValue;
|
|
252
256
|
}
|
|
253
|
-
|
|
257
|
+
else {
|
|
258
|
+
const nextValue = resolved.remainder[index + 1];
|
|
259
|
+
if (!nextValue || nextValue.startsWith("--")) {
|
|
260
|
+
throw usageProblem(`Flag '--${rawName}' requires a value.`, definition.path.join(" "));
|
|
261
|
+
}
|
|
262
|
+
valueSource = nextValue;
|
|
254
263
|
index += 1;
|
|
255
264
|
}
|
|
256
265
|
const coercedValue = coerceFlagValue(flag.valueType ?? "string", valueSource, rawName, definition.path.join(" "));
|
package/dist/session-store.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { mkdir, open, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
1
3
|
import { readSessionFile, removeSessionFile, writeSessionFile } from "./session.js";
|
|
4
|
+
const REFRESH_LOCK_STALE_MS = 30_000;
|
|
5
|
+
const REFRESH_LOCK_RETRY_MS = 25;
|
|
2
6
|
export function createFileSessionStore(path) {
|
|
7
|
+
const refreshLockPath = `${path}.refresh.lock`;
|
|
3
8
|
return {
|
|
4
9
|
async clear() {
|
|
5
10
|
await removeSessionFile(path);
|
|
@@ -9,11 +14,15 @@ export function createFileSessionStore(path) {
|
|
|
9
14
|
},
|
|
10
15
|
async write(session) {
|
|
11
16
|
await writeSessionFile(path, session);
|
|
17
|
+
},
|
|
18
|
+
async withRefreshLock(operation) {
|
|
19
|
+
return withFileRefreshLock(refreshLockPath, operation);
|
|
12
20
|
}
|
|
13
21
|
};
|
|
14
22
|
}
|
|
15
23
|
export function createMemorySessionStore(initial = null) {
|
|
16
24
|
let current = initial;
|
|
25
|
+
let refreshLock = Promise.resolve();
|
|
17
26
|
return {
|
|
18
27
|
async clear() {
|
|
19
28
|
current = null;
|
|
@@ -23,6 +32,77 @@ export function createMemorySessionStore(initial = null) {
|
|
|
23
32
|
},
|
|
24
33
|
async write(session) {
|
|
25
34
|
current = session;
|
|
35
|
+
},
|
|
36
|
+
async withRefreshLock(operation) {
|
|
37
|
+
const previous = refreshLock;
|
|
38
|
+
let release;
|
|
39
|
+
refreshLock = new Promise((resolve) => {
|
|
40
|
+
release = resolve;
|
|
41
|
+
});
|
|
42
|
+
await previous;
|
|
43
|
+
try {
|
|
44
|
+
return await operation();
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
release();
|
|
48
|
+
}
|
|
26
49
|
}
|
|
27
50
|
};
|
|
28
51
|
}
|
|
52
|
+
async function withFileRefreshLock(lockPath, operation) {
|
|
53
|
+
const release = await acquireFileRefreshLock(lockPath);
|
|
54
|
+
try {
|
|
55
|
+
return await operation();
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await release();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function acquireFileRefreshLock(lockPath) {
|
|
62
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
63
|
+
while (true) {
|
|
64
|
+
try {
|
|
65
|
+
const handle = await open(lockPath, "wx", 0o600);
|
|
66
|
+
try {
|
|
67
|
+
await handle.writeFile(`${String(process.pid)} ${new Date().toISOString()}\n`);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
await handle.close();
|
|
71
|
+
}
|
|
72
|
+
return async () => {
|
|
73
|
+
await rm(lockPath, { force: true });
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!isFileExistsError(error)) {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
await removeStaleFileRefreshLock(lockPath);
|
|
81
|
+
await sleep(REFRESH_LOCK_RETRY_MS);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function removeStaleFileRefreshLock(lockPath) {
|
|
86
|
+
try {
|
|
87
|
+
const fileStat = await stat(lockPath);
|
|
88
|
+
if (Date.now() - fileStat.mtimeMs > REFRESH_LOCK_STALE_MS) {
|
|
89
|
+
await rm(lockPath, { force: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (!isMissingFileError(error)) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function sleep(ms) {
|
|
99
|
+
await new Promise((resolve) => {
|
|
100
|
+
setTimeout(resolve, ms);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function isFileExistsError(error) {
|
|
104
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EEXIST");
|
|
105
|
+
}
|
|
106
|
+
function isMissingFileError(error) {
|
|
107
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
108
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CliSessionState } from "./session.js";
|
|
2
|
+
import type { SessionStore } from "./transport.js";
|
|
3
|
+
export declare function refreshSessionWithCoordination(input: {
|
|
4
|
+
isRefreshTokenInvalidError: (error: unknown) => boolean;
|
|
5
|
+
refreshSessionDirect: (session: CliSessionState) => Promise<CliSessionState>;
|
|
6
|
+
session: CliSessionState;
|
|
7
|
+
sessionStore: SessionStore;
|
|
8
|
+
shouldRefresh: (session: CliSessionState) => boolean;
|
|
9
|
+
}): Promise<CliSessionState>;
|
|
10
|
+
export declare function resolveEffectiveSession(requestSession: CliSessionState | null, sessionStore: SessionStore): Promise<CliSessionState | null>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export async function refreshSessionWithCoordination(input) {
|
|
2
|
+
return withRefreshLock(input.sessionStore, async () => {
|
|
3
|
+
const effectiveSession = (await resolveEffectiveSession(input.session, input.sessionStore)) ?? input.session;
|
|
4
|
+
if (isSessionNewer(effectiveSession, input.session) && !input.shouldRefresh(effectiveSession)) {
|
|
5
|
+
return effectiveSession;
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
return await input.refreshSessionDirect(effectiveSession);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (input.isRefreshTokenInvalidError(error)) {
|
|
12
|
+
const recoveredSession = await resolveEffectiveSession(input.session, input.sessionStore);
|
|
13
|
+
if (recoveredSession &&
|
|
14
|
+
isSessionNewer(recoveredSession, input.session) &&
|
|
15
|
+
!input.shouldRefresh(recoveredSession)) {
|
|
16
|
+
return recoveredSession;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async function withRefreshLock(sessionStore, operation) {
|
|
24
|
+
return sessionStore.withRefreshLock
|
|
25
|
+
? sessionStore.withRefreshLock(operation)
|
|
26
|
+
: operation();
|
|
27
|
+
}
|
|
28
|
+
export async function resolveEffectiveSession(requestSession, sessionStore) {
|
|
29
|
+
if (!requestSession) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const storedSession = await sessionStore.read();
|
|
33
|
+
if (!storedSession || !isSameSessionIdentity(storedSession, requestSession)) {
|
|
34
|
+
return requestSession;
|
|
35
|
+
}
|
|
36
|
+
return isSessionNewer(storedSession, requestSession) ? storedSession : requestSession;
|
|
37
|
+
}
|
|
38
|
+
function isSameSessionIdentity(candidate, current) {
|
|
39
|
+
return candidate.baseUrl === current.baseUrl
|
|
40
|
+
&& candidate.user.id === current.user.id
|
|
41
|
+
&& candidate.accessTokenPayload.sub === current.accessTokenPayload.sub;
|
|
42
|
+
}
|
|
43
|
+
function isSessionNewer(candidate, current) {
|
|
44
|
+
if (candidate.accessToken === current.accessToken && candidate.refreshToken === current.refreshToken) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (candidate.accessTokenPayload.exp !== current.accessTokenPayload.exp) {
|
|
48
|
+
return candidate.accessTokenPayload.exp > current.accessTokenPayload.exp;
|
|
49
|
+
}
|
|
50
|
+
return candidate.accessTokenPayload.iat >= current.accessTokenPayload.iat;
|
|
51
|
+
}
|
package/dist/transport.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type CliSessionState } from "./session.js";
|
|
|
2
2
|
export interface SessionStore {
|
|
3
3
|
clear(): Promise<void>;
|
|
4
4
|
read(): Promise<CliSessionState | null>;
|
|
5
|
+
withRefreshLock?<T>(operation: () => Promise<T>): Promise<T>;
|
|
5
6
|
write(session: CliSessionState): Promise<void>;
|
|
6
7
|
}
|
|
7
8
|
export interface CliAuthSettings {
|
|
@@ -10,6 +11,19 @@ export interface CliAuthSettings {
|
|
|
10
11
|
oidcEnabled: boolean;
|
|
11
12
|
selfServiceOrgCreationEnabled: boolean;
|
|
12
13
|
}
|
|
14
|
+
export interface CliDeviceLoginStart {
|
|
15
|
+
deviceCode: string;
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
pollIntervalSeconds: number;
|
|
18
|
+
userCode: string;
|
|
19
|
+
verificationPath: string;
|
|
20
|
+
}
|
|
21
|
+
export type CliDeviceLoginClaim = {
|
|
22
|
+
session: CliSessionState;
|
|
23
|
+
status: "approved";
|
|
24
|
+
} | {
|
|
25
|
+
status: "denied" | "expired" | "pending";
|
|
26
|
+
};
|
|
13
27
|
export type ApiErrorDetails = Record<string, unknown>;
|
|
14
28
|
type RequestBody = unknown | ((session: CliSessionState) => unknown);
|
|
15
29
|
type BytesRequest = {
|
|
@@ -26,6 +40,7 @@ type BytesRequest = {
|
|
|
26
40
|
validateHeaders?: (headers: Headers) => void;
|
|
27
41
|
};
|
|
28
42
|
export declare class ApiError extends Error {
|
|
43
|
+
readonly code: string;
|
|
29
44
|
readonly detail: string;
|
|
30
45
|
readonly details?: ApiErrorDetails;
|
|
31
46
|
readonly instance: string;
|
|
@@ -34,6 +49,7 @@ export declare class ApiError extends Error {
|
|
|
34
49
|
readonly title: string;
|
|
35
50
|
readonly type: string;
|
|
36
51
|
constructor(input: {
|
|
52
|
+
code?: string;
|
|
37
53
|
detail: string;
|
|
38
54
|
details?: ApiErrorDetails;
|
|
39
55
|
instance: string;
|
|
@@ -59,6 +75,11 @@ export declare function createPlatformTransport(input: {
|
|
|
59
75
|
name: string;
|
|
60
76
|
password: string;
|
|
61
77
|
}): Promise<CliSessionState>;
|
|
78
|
+
startDeviceLogin(baseUrl: string): Promise<CliDeviceLoginStart>;
|
|
79
|
+
claimDeviceLogin(claim: {
|
|
80
|
+
baseUrl: string;
|
|
81
|
+
deviceCode: string;
|
|
82
|
+
}): Promise<CliDeviceLoginClaim>;
|
|
62
83
|
refreshSession(session: CliSessionState): Promise<CliSessionState>;
|
|
63
84
|
requestJson<T>(request: {
|
|
64
85
|
body?: RequestBody;
|