@kora-platform/cli 0.8.0-rc6 → 0.8.0-rc8
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 +7 -0
- package/dist/api-client.js +9 -0
- package/dist/api-types.d.ts +1 -1
- package/dist/command-flags.d.ts +1 -0
- package/dist/command-flags.js +7 -0
- package/dist/command-registry.js +22 -5
- package/dist/commands.js +25 -1
- package/dist/runner.js +13 -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 +1 -0
- package/dist/transport.js +32 -40
- package/package.json +1 -1
package/dist/api-client.d.ts
CHANGED
|
@@ -96,6 +96,13 @@ export declare function createPlatformApiClient(input: PlatformApiClientInput):
|
|
|
96
96
|
}): Promise<{
|
|
97
97
|
variables: RuntimeVariableRecord[];
|
|
98
98
|
}>;
|
|
99
|
+
upsertRuntimeVariable(session: CliSessionState, orgId: string, inputData: {
|
|
100
|
+
environment: string;
|
|
101
|
+
name: string;
|
|
102
|
+
value: string;
|
|
103
|
+
}): Promise<{
|
|
104
|
+
variable: RuntimeVariableRecord;
|
|
105
|
+
}>;
|
|
99
106
|
listOrgSecrets(session: CliSessionState, orgId: string, inputData?: {
|
|
100
107
|
environment?: string;
|
|
101
108
|
}): Promise<{
|
package/dist/api-client.js
CHANGED
|
@@ -98,6 +98,15 @@ export function createPlatformApiClient(input) {
|
|
|
98
98
|
session
|
|
99
99
|
});
|
|
100
100
|
},
|
|
101
|
+
async upsertRuntimeVariable(session, orgId, inputData) {
|
|
102
|
+
const { environment, name, value } = inputData;
|
|
103
|
+
return transport.requestJson({
|
|
104
|
+
body: { value },
|
|
105
|
+
method: "PUT",
|
|
106
|
+
path: withQuery(`/api/v1/orgs/${encodeURIComponent(orgId)}/runtime-variables/${encodeURIComponent(name)}`, { environment }),
|
|
107
|
+
session
|
|
108
|
+
});
|
|
109
|
+
},
|
|
101
110
|
async listOrgSecrets(session, orgId, inputData = {}) {
|
|
102
111
|
return transport.requestJson({
|
|
103
112
|
path: withQuery(`/api/v1/orgs/${encodeURIComponent(orgId)}/secrets`, inputData),
|
package/dist/api-types.d.ts
CHANGED
|
@@ -340,7 +340,7 @@ export interface RuntimeControlRunDetailRecord extends RuntimeControlRunRecord {
|
|
|
340
340
|
failureLookupError?: string;
|
|
341
341
|
historyLength?: number;
|
|
342
342
|
historySizeBytes?: number;
|
|
343
|
-
state?: CliRecord;
|
|
343
|
+
state?: CliRecord | null;
|
|
344
344
|
stateQueryError?: string;
|
|
345
345
|
}
|
|
346
346
|
export interface RuntimeControlRunStateRecord extends CliRecord {
|
package/dist/command-flags.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ParsedCommand } from "./runner.js";
|
|
2
2
|
export declare function readOptionalStringFlag(parsed: ParsedCommand, name: string): string | undefined;
|
|
3
3
|
export declare function readRequiredStringFlag(parsed: ParsedCommand, name: string): string;
|
|
4
|
+
export declare function readRequiredStringFlagPreservingEmpty(parsed: ParsedCommand, name: string): string;
|
|
4
5
|
export declare function readOptionalNumberFlag(parsed: ParsedCommand, name: string): number | undefined;
|
package/dist/command-flags.js
CHANGED
|
@@ -10,6 +10,13 @@ export function readRequiredStringFlag(parsed, name) {
|
|
|
10
10
|
}
|
|
11
11
|
return value;
|
|
12
12
|
}
|
|
13
|
+
export function readRequiredStringFlagPreservingEmpty(parsed, name) {
|
|
14
|
+
const value = parsed.flags[name];
|
|
15
|
+
if (typeof value !== "string") {
|
|
16
|
+
throw usageProblem(`Missing required flag --${name}.`, parsed.definition.id);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
13
20
|
export function readOptionalNumberFlag(parsed, name) {
|
|
14
21
|
const value = parsed.flags[name];
|
|
15
22
|
return typeof value === "number" ? value : undefined;
|
package/dist/command-registry.js
CHANGED
|
@@ -755,7 +755,7 @@ const RAW_CLI_COMMANDS = [
|
|
|
755
755
|
requiresActiveOrg: true
|
|
756
756
|
}),
|
|
757
757
|
command(["access", "members", "list"], "List organization members.", {
|
|
758
|
-
labels: ["read", "access"],
|
|
758
|
+
labels: ["read", "chat-read", "access"],
|
|
759
759
|
flags: [
|
|
760
760
|
flag("limit", "Maximum number of members to return.", {
|
|
761
761
|
acceptsValue: true,
|
|
@@ -770,7 +770,7 @@ const RAW_CLI_COMMANDS = [
|
|
|
770
770
|
requiresActiveOrg: true
|
|
771
771
|
}),
|
|
772
772
|
command(["access", "members", "get"], "Show one organization member.", {
|
|
773
|
-
labels: ["read", "access"],
|
|
773
|
+
labels: ["read", "chat-read", "access"],
|
|
774
774
|
args: [arg("user-id", "User id.")],
|
|
775
775
|
requiresActiveOrg: true
|
|
776
776
|
}),
|
|
@@ -793,7 +793,7 @@ const RAW_CLI_COMMANDS = [
|
|
|
793
793
|
requiresActiveOrg: true
|
|
794
794
|
}),
|
|
795
795
|
command(["access", "invites", "list"], "List organization invites.", {
|
|
796
|
-
labels: ["read", "access"],
|
|
796
|
+
labels: ["read", "chat-read", "access"],
|
|
797
797
|
flags: [
|
|
798
798
|
flag("limit", "Maximum number of invites to return.", {
|
|
799
799
|
acceptsValue: true,
|
|
@@ -808,12 +808,12 @@ const RAW_CLI_COMMANDS = [
|
|
|
808
808
|
requiresActiveOrg: true
|
|
809
809
|
}),
|
|
810
810
|
command(["access", "invites", "get"], "Show one organization invite.", {
|
|
811
|
-
labels: ["read", "access"],
|
|
811
|
+
labels: ["read", "chat-read", "access"],
|
|
812
812
|
args: [arg("invite-id", "Invite id.")],
|
|
813
813
|
requiresActiveOrg: true
|
|
814
814
|
}),
|
|
815
815
|
command(["access", "invites", "create"], "Create an organization invite.", {
|
|
816
|
-
labels: ["write", "access"],
|
|
816
|
+
labels: ["write", "chat-write", "access"],
|
|
817
817
|
flags: [
|
|
818
818
|
flag("email", "Invitee email address.", {
|
|
819
819
|
acceptsValue: true,
|
|
@@ -1107,6 +1107,23 @@ const RAW_CLI_COMMANDS = [
|
|
|
1107
1107
|
})],
|
|
1108
1108
|
requiresActiveOrg: true
|
|
1109
1109
|
}),
|
|
1110
|
+
command(["env", "set"], "Create or update one runtime variable without reading other values.", {
|
|
1111
|
+
labels: ["write", "chat-write", "env"],
|
|
1112
|
+
args: [arg("name", "Variable name.")],
|
|
1113
|
+
flags: [
|
|
1114
|
+
flag("environment", "Target environment.", {
|
|
1115
|
+
acceptsValue: true,
|
|
1116
|
+
required: true,
|
|
1117
|
+
valueType: "string"
|
|
1118
|
+
}),
|
|
1119
|
+
flag("value", "Variable value.", {
|
|
1120
|
+
acceptsValue: true,
|
|
1121
|
+
required: true,
|
|
1122
|
+
valueType: "string"
|
|
1123
|
+
})
|
|
1124
|
+
],
|
|
1125
|
+
requiresActiveOrg: true
|
|
1126
|
+
}),
|
|
1110
1127
|
command(["env", "replace"], "Replace the full runtime environment from JSON.", {
|
|
1111
1128
|
labels: ["destructive", "env"],
|
|
1112
1129
|
destructive: true,
|
package/dist/commands.js
CHANGED
|
@@ -3,7 +3,7 @@ import { executeAuthLogin, executeAuthLogout, executeAuthSignup, executeAuthWhoa
|
|
|
3
3
|
import { getCliSchema, listCliSchemas } from "./schema-registry.js";
|
|
4
4
|
import { createPlatformApiClient } from "./api-client.js";
|
|
5
5
|
import { authProblem, genericProblem, notFoundProblem, usageProblem } from "./cli-errors.js";
|
|
6
|
-
import { readOptionalNumberFlag, readOptionalStringFlag, readRequiredStringFlag } from "./command-flags.js";
|
|
6
|
+
import { readOptionalNumberFlag, readOptionalStringFlag, readRequiredStringFlag, readRequiredStringFlagPreservingEmpty } from "./command-flags.js";
|
|
7
7
|
import { executeArtifactDownload, executeArtifactArchive, executeArtifactInspect, executeArtifactInventory, executeArtifactList, executeArtifactPurge, executeArtifactRead, executeArtifactRestore, executeArtifactUpload } from "./artifact-commands.js";
|
|
8
8
|
import { executeAudit } from "./audit-commands.js";
|
|
9
9
|
import { isZipArchivePath, readArchiveBytes, readImportEntries, readJsonInputSpecifier, readWorkspaceTestEntries, readTextInputSpecifier, writeReleaseSourceFiles } from "./files.js";
|
|
@@ -164,6 +164,7 @@ export async function executeParsedCommand(parsed, context) {
|
|
|
164
164
|
return executeExtensions(parsed, context, api, resolveOrgScope);
|
|
165
165
|
case "env.list":
|
|
166
166
|
case "env.get":
|
|
167
|
+
case "env.set":
|
|
167
168
|
case "env.replace":
|
|
168
169
|
return executeEnv(parsed, context, api);
|
|
169
170
|
case "secrets.list":
|
|
@@ -1224,6 +1225,29 @@ async function executeEnv(parsed, context, api) {
|
|
|
1224
1225
|
meta: { command: parsed.definition.path.join(" "), environment, orgId: org.id }
|
|
1225
1226
|
};
|
|
1226
1227
|
}
|
|
1228
|
+
case "env.set": {
|
|
1229
|
+
const environment = readRequiredCliEnvironmentFlag(parsed);
|
|
1230
|
+
const name = readRequiredArg(parsed, "name");
|
|
1231
|
+
announceResolvedEnvironment(parsed, context, environment);
|
|
1232
|
+
const data = await api.upsertRuntimeVariable(session, org.id, {
|
|
1233
|
+
environment,
|
|
1234
|
+
name,
|
|
1235
|
+
value: readRequiredStringFlagPreservingEmpty(parsed, "value")
|
|
1236
|
+
});
|
|
1237
|
+
return {
|
|
1238
|
+
data: {
|
|
1239
|
+
variable: {
|
|
1240
|
+
environmentKey: data.variable.environmentKey,
|
|
1241
|
+
name: data.variable.name,
|
|
1242
|
+
status: "defined",
|
|
1243
|
+
updatedAt: data.variable.updatedAt
|
|
1244
|
+
}
|
|
1245
|
+
},
|
|
1246
|
+
human: renderSuccess(`Saved runtime variable '${data.variable.name}' in ${environment}.`),
|
|
1247
|
+
kind: "authoring_env_set",
|
|
1248
|
+
meta: { command: parsed.definition.path.join(" "), environment, orgId: org.id }
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1227
1251
|
case "env.replace": {
|
|
1228
1252
|
const environment = readRequiredCliEnvironmentFlag(parsed);
|
|
1229
1253
|
const body = await readJsonInputSpecifier(String(parsed.flags.file), context.stdin, parsed.definition.id, { flag: "file" });
|
package/dist/runner.js
CHANGED
|
@@ -232,7 +232,10 @@ function parseInvocation(tokens, commandFilter) {
|
|
|
232
232
|
positionals.push(token);
|
|
233
233
|
continue;
|
|
234
234
|
}
|
|
235
|
-
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);
|
|
236
239
|
const flag = flagDefinitions.get(rawName);
|
|
237
240
|
if (!flag) {
|
|
238
241
|
const detail = rawName === "output"
|
|
@@ -247,11 +250,16 @@ function parseInvocation(tokens, commandFilter) {
|
|
|
247
250
|
flags[rawName] = true;
|
|
248
251
|
continue;
|
|
249
252
|
}
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
+
let valueSource;
|
|
254
|
+
if (maybeValue !== undefined) {
|
|
255
|
+
valueSource = maybeValue;
|
|
253
256
|
}
|
|
254
|
-
|
|
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;
|
|
255
263
|
index += 1;
|
|
256
264
|
}
|
|
257
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 {
|
package/dist/transport.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizePublicErrorCode, readPublicErrorCodeFromType } from "./error-code.js";
|
|
2
2
|
import { extractRefreshTokenFromHeaders } from "./session.js";
|
|
3
|
+
import { refreshSessionWithCoordination, resolveEffectiveSession } from "./transport-refresh.js";
|
|
3
4
|
export class ApiError extends Error {
|
|
4
5
|
code;
|
|
5
6
|
detail;
|
|
@@ -28,6 +29,13 @@ export class ApiError extends Error {
|
|
|
28
29
|
}
|
|
29
30
|
export function createPlatformTransport(input) {
|
|
30
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
|
+
});
|
|
31
39
|
return {
|
|
32
40
|
async getAuthSettings(baseUrl) {
|
|
33
41
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
@@ -116,18 +124,7 @@ export function createPlatformTransport(input) {
|
|
|
116
124
|
return { session, status: "approved" };
|
|
117
125
|
},
|
|
118
126
|
async refreshSession(session) {
|
|
119
|
-
|
|
120
|
-
body: JSON.stringify({
|
|
121
|
-
refreshToken: session.refreshToken
|
|
122
|
-
}),
|
|
123
|
-
headers: {
|
|
124
|
-
"content-type": "application/json"
|
|
125
|
-
},
|
|
126
|
-
method: "POST"
|
|
127
|
-
});
|
|
128
|
-
const nextSession = await parseSessionResponse(response, session.baseUrl, "/api/v1/auth/refresh", session.activeOrg);
|
|
129
|
-
await input.sessionStore.write(nextSession);
|
|
130
|
-
return nextSession;
|
|
127
|
+
return refreshSession(session);
|
|
131
128
|
},
|
|
132
129
|
async requestJson(request) {
|
|
133
130
|
let session = await resolveEffectiveSession(request.session, input.sessionStore);
|
|
@@ -141,11 +138,11 @@ export function createPlatformTransport(input) {
|
|
|
141
138
|
});
|
|
142
139
|
}
|
|
143
140
|
if (shouldRefresh(session, now) && canRefresh(session)) {
|
|
144
|
-
session = await
|
|
141
|
+
session = await refreshSession(session);
|
|
145
142
|
}
|
|
146
143
|
const firstResponse = await authenticatedFetch(session, request);
|
|
147
144
|
if (firstResponse.status === 401 && canRefresh(session)) {
|
|
148
|
-
session = await
|
|
145
|
+
session = await refreshSession(session);
|
|
149
146
|
return handleJsonResponse(await authenticatedFetch(session, request), request.path);
|
|
150
147
|
}
|
|
151
148
|
return handleJsonResponse(firstResponse, request.path);
|
|
@@ -162,17 +159,36 @@ export function createPlatformTransport(input) {
|
|
|
162
159
|
});
|
|
163
160
|
}
|
|
164
161
|
if (shouldRefresh(session, now) && canRefresh(session)) {
|
|
165
|
-
session = await
|
|
162
|
+
session = await refreshSession(session);
|
|
166
163
|
}
|
|
167
164
|
const firstResponse = await authenticatedFetch(session, request);
|
|
168
165
|
if (firstResponse.status === 401 && canRefresh(session)) {
|
|
169
|
-
session = await
|
|
166
|
+
session = await refreshSession(session);
|
|
170
167
|
return handleBytesResponse(await authenticatedFetch(session, request), request);
|
|
171
168
|
}
|
|
172
169
|
return handleBytesResponse(firstResponse, request);
|
|
173
170
|
}
|
|
174
171
|
};
|
|
175
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
|
+
}
|
|
176
192
|
async function authenticatedFetch(session, request) {
|
|
177
193
|
const body = resolveRequestBody(request.body, session);
|
|
178
194
|
const hasJsonBody = body !== undefined;
|
|
@@ -389,27 +405,3 @@ function shouldRefresh(session, now) {
|
|
|
389
405
|
function canRefresh(session) {
|
|
390
406
|
return session.refreshToken.trim().length > 0;
|
|
391
407
|
}
|
|
392
|
-
async function resolveEffectiveSession(requestSession, sessionStore) {
|
|
393
|
-
if (!requestSession) {
|
|
394
|
-
return null;
|
|
395
|
-
}
|
|
396
|
-
const storedSession = await sessionStore.read();
|
|
397
|
-
if (!storedSession || !isSameSessionIdentity(storedSession, requestSession)) {
|
|
398
|
-
return requestSession;
|
|
399
|
-
}
|
|
400
|
-
return isSessionNewer(storedSession, requestSession) ? storedSession : requestSession;
|
|
401
|
-
}
|
|
402
|
-
function isSameSessionIdentity(candidate, current) {
|
|
403
|
-
return candidate.baseUrl === current.baseUrl
|
|
404
|
-
&& candidate.user.id === current.user.id
|
|
405
|
-
&& candidate.accessTokenPayload.sub === current.accessTokenPayload.sub;
|
|
406
|
-
}
|
|
407
|
-
function isSessionNewer(candidate, current) {
|
|
408
|
-
if (candidate.accessToken === current.accessToken && candidate.refreshToken === current.refreshToken) {
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
if (candidate.accessTokenPayload.exp !== current.accessTokenPayload.exp) {
|
|
412
|
-
return candidate.accessTokenPayload.exp > current.accessTokenPayload.exp;
|
|
413
|
-
}
|
|
414
|
-
return candidate.accessTokenPayload.iat >= current.accessTokenPayload.iat;
|
|
415
|
-
}
|