@linzumi/cli 0.0.1-beta → 0.0.3-beta
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 +82 -105
- package/bin/linzumi.js +16 -8
- package/package.json +4 -2
- package/src/authCache.ts +157 -0
- package/src/authResolution.ts +75 -0
- package/src/channelSession.ts +3248 -0
- package/src/channelSessionSupport.ts +255 -0
- package/src/codexAppServer.ts +380 -0
- package/src/codexOutput.ts +846 -0
- package/src/index.ts +356 -0
- package/src/json.ts +49 -0
- package/src/kandanQueue.ts +102 -0
- package/src/oauth.ts +294 -0
- package/src/phoenix.ts +335 -0
- package/src/protocol.ts +211 -0
- package/src/runner.ts +524 -0
- package/src/runnerConsoleReporter.ts +142 -0
- package/src/runnerLogger.ts +50 -0
- package/src/cli.js +0 -240
package/README.md
CHANGED
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
# @linzumi/cli
|
|
2
2
|
|
|
3
3
|
```text
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
4
|
+
___ ___
|
|
5
|
+
.-' '-. .-' '-.
|
|
6
|
+
.' ' '.
|
|
7
|
+
/ _ _ \
|
|
8
|
+
| .' '. .' '. |
|
|
9
|
+
| / \ .-. / \ |
|
|
10
|
+
\ \ (o o) / /
|
|
11
|
+
'._'._ \_/ _.'_.'
|
|
12
|
+
| | |
|
|
13
|
+
| /|\ |
|
|
14
|
+
|___/ | \___|
|
|
15
|
+
\_|_/
|
|
16
|
+
/ \
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Linzumi's CLI package. It installs the `linzumi` executable.
|
|
20
20
|
|
|
21
|
-
The
|
|
22
|
-
local
|
|
21
|
+
The initial CLI command connects this computer to Kandan as a local Codex
|
|
22
|
+
runner. It wraps the same local runner that was previously started with
|
|
23
|
+
`bun run start -- ...`.
|
|
23
24
|
|
|
24
25
|
## Install
|
|
25
26
|
|
|
26
27
|
Install the exact beta version:
|
|
27
28
|
|
|
28
29
|
```bash
|
|
29
|
-
npm install -g @linzumi/cli@0.0.
|
|
30
|
+
npm install -g @linzumi/cli@0.0.3-beta
|
|
30
31
|
```
|
|
31
32
|
|
|
32
33
|
Or install the current beta tag:
|
|
@@ -35,68 +36,76 @@ Or install the current beta tag:
|
|
|
35
36
|
npm install -g @linzumi/cli@beta
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
This beta expects Bun and Codex to be available locally:
|
|
39
40
|
|
|
40
41
|
```bash
|
|
42
|
+
bun --version
|
|
43
|
+
codex --version
|
|
41
44
|
linzumi --version
|
|
42
45
|
```
|
|
43
46
|
|
|
44
|
-
Expected output
|
|
47
|
+
Expected CLI output:
|
|
45
48
|
|
|
46
49
|
```bash
|
|
47
|
-
linzumi 0.0.
|
|
50
|
+
linzumi 0.0.3-beta
|
|
48
51
|
```
|
|
49
52
|
|
|
50
|
-
##
|
|
53
|
+
## Prod Quick Start
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
For the Linzumi workspace in prod, this is the first command to try:
|
|
53
56
|
|
|
54
57
|
```bash
|
|
55
|
-
linzumi
|
|
58
|
+
npm install -g @linzumi/cli@0.0.3-beta
|
|
59
|
+
|
|
60
|
+
linzumi connect \
|
|
61
|
+
--kandan-url wss://serve.kandanai.com \
|
|
62
|
+
--workspace linzumi \
|
|
63
|
+
--channel seans-playground \
|
|
64
|
+
--listen-user sean \
|
|
65
|
+
--codex-bin codex \
|
|
66
|
+
--launch-tui
|
|
56
67
|
```
|
|
57
68
|
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
The runner handles OAuth itself. If the cached Kandan token is missing or
|
|
70
|
+
rejected, it opens the OAuth flow and saves the refreshed auth cache.
|
|
60
71
|
|
|
61
|
-
|
|
72
|
+
For a more explicit launch that pins the Codex model, reasoning effort, and
|
|
73
|
+
fast service tier:
|
|
62
74
|
|
|
63
75
|
```bash
|
|
64
|
-
linzumi connect
|
|
76
|
+
linzumi connect \
|
|
77
|
+
--kandan-url wss://serve.kandanai.com \
|
|
78
|
+
--workspace linzumi \
|
|
79
|
+
--channel seans-playground \
|
|
80
|
+
--listen-user sean \
|
|
81
|
+
--codex-bin codex \
|
|
82
|
+
--model gpt-5.5 \
|
|
83
|
+
--reasoning-effort low \
|
|
84
|
+
--fast \
|
|
85
|
+
--launch-tui
|
|
65
86
|
```
|
|
66
87
|
|
|
67
|
-
|
|
68
|
-
computer; Kandan does not execute this binary for you. Kandan shows the command
|
|
69
|
-
or connection details, and you run them locally.
|
|
70
|
-
|
|
71
|
-
## Connect To The Linzumi Workspace
|
|
88
|
+
## Basic Use
|
|
72
89
|
|
|
73
|
-
|
|
90
|
+
Running `linzumi` without arguments prints the local runner guide:
|
|
74
91
|
|
|
75
92
|
```bash
|
|
76
|
-
|
|
93
|
+
linzumi
|
|
77
94
|
```
|
|
78
95
|
|
|
79
|
-
|
|
96
|
+
Start the runner for a Kandan workspace/channel:
|
|
80
97
|
|
|
81
98
|
```bash
|
|
82
|
-
linzumi
|
|
99
|
+
linzumi connect \
|
|
100
|
+
--kandan-url wss://serve.kandanai.com \
|
|
101
|
+
--workspace linzumi \
|
|
102
|
+
--channel seans-playground \
|
|
103
|
+
--listen-user sean \
|
|
104
|
+
--codex-bin codex \
|
|
105
|
+
--launch-tui
|
|
83
106
|
```
|
|
84
107
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
4. Start the local runner connection flow in Kandan.
|
|
88
|
-
|
|
89
|
-
5. Copy the command Kandan shows you and run it in a terminal on this computer.
|
|
90
|
-
|
|
91
|
-
6. Leave that terminal process running while you use the local runner from
|
|
92
|
-
Kandan.
|
|
93
|
-
|
|
94
|
-
For orientation, running the CLI without arguments prints the local connection
|
|
95
|
-
guide:
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
linzumi
|
|
99
|
-
```
|
|
108
|
+
Leave that terminal process running while Kandan uses the local runner.
|
|
100
109
|
|
|
101
110
|
## Commands
|
|
102
111
|
|
|
@@ -104,74 +113,42 @@ Supported commands right now:
|
|
|
104
113
|
|
|
105
114
|
```bash
|
|
106
115
|
linzumi
|
|
107
|
-
linzumi connect
|
|
108
116
|
linzumi --help
|
|
109
117
|
linzumi --version
|
|
110
118
|
linzumi connect --help
|
|
111
|
-
linzumi connect
|
|
119
|
+
linzumi connect [runner options]
|
|
120
|
+
linzumi auth [auth options]
|
|
112
121
|
```
|
|
113
122
|
|
|
114
|
-
`local-codex-runner` is
|
|
123
|
+
`linzumi local-codex-runner` is accepted as a compatibility alias for
|
|
124
|
+
`linzumi connect`.
|
|
115
125
|
|
|
116
|
-
|
|
117
|
-
linzumi local-codex-runner
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## Runner Protocol
|
|
121
|
-
|
|
122
|
-
During the connection flow, Kandan may tell you to run a command shaped like:
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
linzumi connect app-server --listen ws://127.0.0.1:45021
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
That trailing `app-server --listen <url>` is runner protocol. It is not a
|
|
129
|
-
separate Linzumi feature; it is the Codex app-server mode that lets this local
|
|
130
|
-
process open the websocket Kandan will connect to.
|
|
131
|
-
|
|
132
|
-
## Configuration
|
|
133
|
-
|
|
134
|
-
The Kandan local runner flow should give you the command or configuration to run
|
|
135
|
-
on your machine. For manual testing, the same configuration can be supplied with
|
|
136
|
-
environment variables:
|
|
126
|
+
## Useful Options
|
|
137
127
|
|
|
138
128
|
```bash
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
129
|
+
--kandan-url <ws-url> Kandan websocket URL, for example wss://serve.kandanai.com
|
|
130
|
+
--workspace <slug> Workspace slug, for example linzumi
|
|
131
|
+
--channel <slug|w/c> Channel slug, or workspace/channel
|
|
132
|
+
--kandan-thread-id <uuid> Resume an existing Kandan thread
|
|
133
|
+
--listen-user <user|all> User whose replies are accepted, or all
|
|
134
|
+
--cwd <path> Working directory for Codex
|
|
135
|
+
--codex-bin <path> Codex executable, default codex
|
|
136
|
+
--model <name> Codex model
|
|
137
|
+
--reasoning-effort <value> Codex reasoning effort
|
|
138
|
+
--fast Request the fast service tier
|
|
139
|
+
--launch-tui Launch codex --remote against the app-server
|
|
140
|
+
--oauth-callback-host <ip> Callback host reachable by your browser
|
|
143
141
|
```
|
|
144
142
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
```bash
|
|
148
|
-
linzumi connect \
|
|
149
|
-
--linz-bin /path/to/kandan-local-bridge-linz \
|
|
150
|
-
--vm-id lzrunner123 \
|
|
151
|
-
--remote-codex-bin .kandan/tools/codex/0.116.0/bin/codex \
|
|
152
|
-
--remote-env .kandan/runtime/agents/13/channels/8/codex_app_server/runtime.env.sh \
|
|
153
|
-
app-server --listen ws://127.0.0.1:45021
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
The runner executes:
|
|
157
|
-
|
|
158
|
-
```bash
|
|
159
|
-
<linz-bin> exec <vm-id> -- sh -lc "exec <remote-kandan-linz> app-server --env-file <remote-env> <remote-codex-bin> <listen-url>"
|
|
160
|
-
```
|
|
143
|
+
Run `linzumi connect --help` for the full option list.
|
|
161
144
|
|
|
162
145
|
## Troubleshooting
|
|
163
146
|
|
|
164
147
|
If `linzumi` is not found, confirm the global npm bin directory is on your
|
|
165
|
-
`PATH
|
|
166
|
-
|
|
167
|
-
```bash
|
|
168
|
-
npm bin -g
|
|
169
|
-
```
|
|
148
|
+
`PATH`.
|
|
170
149
|
|
|
171
|
-
If `
|
|
172
|
-
|
|
173
|
-
need the environment variables or flags listed above.
|
|
150
|
+
If `bun` is not found, install Bun first. This beta wraps the existing Bun local
|
|
151
|
+
runner implementation instead of a Node rewrite.
|
|
174
152
|
|
|
175
|
-
If
|
|
176
|
-
|
|
177
|
-
runner VM.
|
|
153
|
+
If OAuth opens but does not return to the CLI, pass `--oauth-callback-host` with
|
|
154
|
+
an IP address that your browser can reach, such as your Tailscale IP.
|
package/bin/linzumi.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
|
+
const entrypoint = join(packageRoot, "src", "index.ts");
|
|
9
|
+
const result = spawnSync("bun", ["run", entrypoint, ...process.argv.slice(2)], {
|
|
8
10
|
cwd: process.cwd(),
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
env: process.env,
|
|
12
|
+
stdio: "inherit",
|
|
11
13
|
});
|
|
12
14
|
|
|
13
|
-
if (result.
|
|
14
|
-
process.
|
|
15
|
+
if (result.error !== undefined) {
|
|
16
|
+
process.stderr.write(
|
|
17
|
+
`failed to launch linzumi runner with bun: ${result.error.message}\n` +
|
|
18
|
+
"Install Bun, then rerun the linzumi command.\n",
|
|
19
|
+
);
|
|
20
|
+
process.exit(127);
|
|
15
21
|
}
|
|
22
|
+
|
|
23
|
+
process.exit(result.status ?? 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linzumi/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3-beta",
|
|
4
4
|
"description": "Linzumi local Codex runner CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"
|
|
15
|
+
"start": "bun run src/index.ts",
|
|
16
|
+
"test": "bun test"
|
|
16
17
|
},
|
|
17
18
|
"engines": {
|
|
19
|
+
"bun": ">=1.1.0",
|
|
18
20
|
"node": ">=22.0.0"
|
|
19
21
|
},
|
|
20
22
|
"publishConfig": {
|
package/src/authCache.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/*
|
|
2
|
+
- Date: 2026-04-24
|
|
3
|
+
Spec: plans/2026-04-24-local-codex-channel-thread-binding-spec.md
|
|
4
|
+
Relationship: Stores scoped local-runner OAuth tokens so users can log in
|
|
5
|
+
once and then start local Codex runners without pasting tokens repeatedly.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { kandanHttpBaseUrl } from "./oauth";
|
|
11
|
+
|
|
12
|
+
export type CachedLocalRunnerToken = {
|
|
13
|
+
readonly accessToken: string;
|
|
14
|
+
readonly kandanBaseUrl: string;
|
|
15
|
+
readonly issuedAt: string;
|
|
16
|
+
readonly expiresAt?: string | undefined;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type AuthFile = {
|
|
20
|
+
readonly version: 1;
|
|
21
|
+
readonly local_codex_runner?: Record<string, CachedTokenJson> | undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type CachedTokenJson = {
|
|
25
|
+
readonly access_token: string;
|
|
26
|
+
readonly issued_at: string;
|
|
27
|
+
readonly expires_at?: string | undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function defaultAuthFilePath(): string {
|
|
31
|
+
const base = process.env.KANDAN_HOME ?? join(homedir(), ".kandan");
|
|
32
|
+
return join(base, "auth.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readCachedLocalRunnerToken(
|
|
36
|
+
kandanUrl: string,
|
|
37
|
+
authFilePath: string = defaultAuthFilePath(),
|
|
38
|
+
): CachedLocalRunnerToken | undefined {
|
|
39
|
+
if (!existsSync(authFilePath)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const authFile = parseAuthFile(readFileSync(authFilePath, "utf8"));
|
|
44
|
+
const kandanBaseUrl = kandanHttpBaseUrl(kandanUrl);
|
|
45
|
+
const entry = authFile.local_codex_runner?.[kandanBaseUrl];
|
|
46
|
+
|
|
47
|
+
if (entry === undefined || entry.access_token.trim() === "") {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (entry.expires_at !== undefined && Date.parse(entry.expires_at) <= Date.now()) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
accessToken: entry.access_token,
|
|
57
|
+
kandanBaseUrl,
|
|
58
|
+
issuedAt: entry.issued_at,
|
|
59
|
+
expiresAt: entry.expires_at,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function writeCachedLocalRunnerToken(args: {
|
|
64
|
+
readonly kandanUrl: string;
|
|
65
|
+
readonly accessToken: string;
|
|
66
|
+
readonly expiresInSeconds?: number | undefined;
|
|
67
|
+
readonly authFilePath?: string | undefined;
|
|
68
|
+
}): CachedLocalRunnerToken {
|
|
69
|
+
const authFilePath = args.authFilePath ?? defaultAuthFilePath();
|
|
70
|
+
const existing = existsSync(authFilePath)
|
|
71
|
+
? parseAuthFile(readFileSync(authFilePath, "utf8"))
|
|
72
|
+
: { version: 1 as const };
|
|
73
|
+
const kandanBaseUrl = kandanHttpBaseUrl(args.kandanUrl);
|
|
74
|
+
const issuedAt = new Date();
|
|
75
|
+
const expiresAt =
|
|
76
|
+
args.expiresInSeconds === undefined
|
|
77
|
+
? undefined
|
|
78
|
+
: new Date(issuedAt.getTime() + args.expiresInSeconds * 1000).toISOString();
|
|
79
|
+
const next: AuthFile = {
|
|
80
|
+
...existing,
|
|
81
|
+
version: 1,
|
|
82
|
+
local_codex_runner: {
|
|
83
|
+
...(existing.local_codex_runner ?? {}),
|
|
84
|
+
[kandanBaseUrl]: {
|
|
85
|
+
access_token: args.accessToken,
|
|
86
|
+
issued_at: issuedAt.toISOString(),
|
|
87
|
+
expires_at: expiresAt,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
mkdirSync(dirname(authFilePath), { recursive: true });
|
|
93
|
+
writeFileSync(authFilePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
accessToken: args.accessToken,
|
|
97
|
+
kandanBaseUrl,
|
|
98
|
+
issuedAt: issuedAt.toISOString(),
|
|
99
|
+
expiresAt,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseAuthFile(text: string): AuthFile {
|
|
104
|
+
const parsed: unknown = JSON.parse(text);
|
|
105
|
+
|
|
106
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
107
|
+
throw new Error("Kandan auth cache must contain a JSON object");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const value = parsed as Record<string, unknown>;
|
|
111
|
+
const localRunner = value.local_codex_runner;
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
localRunner !== undefined &&
|
|
115
|
+
(typeof localRunner !== "object" || localRunner === null || Array.isArray(localRunner))
|
|
116
|
+
) {
|
|
117
|
+
throw new Error("Kandan auth cache local_codex_runner must be an object");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
version: 1,
|
|
122
|
+
local_codex_runner: normalizeLocalRunnerCache(localRunner),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeLocalRunnerCache(value: unknown): Record<string, CachedTokenJson> | undefined {
|
|
127
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return Object.fromEntries(
|
|
132
|
+
Object.entries(value).flatMap(([key, entry]) => {
|
|
133
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const token = (entry as Record<string, unknown>).access_token;
|
|
138
|
+
const issuedAt = (entry as Record<string, unknown>).issued_at;
|
|
139
|
+
const expiresAt = (entry as Record<string, unknown>).expires_at;
|
|
140
|
+
|
|
141
|
+
if (typeof token !== "string" || typeof issuedAt !== "string") {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return [
|
|
146
|
+
[
|
|
147
|
+
key,
|
|
148
|
+
{
|
|
149
|
+
access_token: token,
|
|
150
|
+
issued_at: issuedAt,
|
|
151
|
+
expires_at: typeof expiresAt === "string" ? expiresAt : undefined,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
];
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*
|
|
2
|
+
- Date: 2026-04-24
|
|
3
|
+
Spec: plans/2026-04-24-local-codex-channel-thread-binding-spec.md
|
|
4
|
+
Relationship: Implements the one-command runner auth behavior: use an
|
|
5
|
+
explicit token when supplied, validate cached scoped runner auth, and fall
|
|
6
|
+
back to the OAuth flow when cached auth is missing or rejected.
|
|
7
|
+
*/
|
|
8
|
+
import { readCachedLocalRunnerToken, writeCachedLocalRunnerToken } from "./authCache";
|
|
9
|
+
import { acquireLocalRunnerTokenDetails, validateLocalRunnerToken } from "./oauth";
|
|
10
|
+
|
|
11
|
+
export type LocalRunnerTokenResolutionArgs = {
|
|
12
|
+
readonly kandanUrl: string;
|
|
13
|
+
readonly explicitToken?: string | undefined;
|
|
14
|
+
readonly workspaceSlug?: string | undefined;
|
|
15
|
+
readonly channelSlug?: string | undefined;
|
|
16
|
+
readonly authFilePath?: string | undefined;
|
|
17
|
+
readonly callbackHost?: string | undefined;
|
|
18
|
+
readonly reportRejectedCachedToken?: (() => void) | undefined;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type LocalRunnerTokenResolutionDeps = {
|
|
22
|
+
readonly readCachedToken: typeof readCachedLocalRunnerToken;
|
|
23
|
+
readonly validateToken: typeof validateLocalRunnerToken;
|
|
24
|
+
readonly acquireAndCacheToken: (args: LocalRunnerTokenResolutionArgs) => Promise<string>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export async function resolveLocalRunnerToken(
|
|
28
|
+
args: LocalRunnerTokenResolutionArgs,
|
|
29
|
+
deps: LocalRunnerTokenResolutionDeps = {
|
|
30
|
+
readCachedToken: readCachedLocalRunnerToken,
|
|
31
|
+
validateToken: validateLocalRunnerToken,
|
|
32
|
+
acquireAndCacheToken,
|
|
33
|
+
},
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
if (args.explicitToken !== undefined) {
|
|
36
|
+
return args.explicitToken;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const cached = deps.readCachedToken(args.kandanUrl, args.authFilePath);
|
|
40
|
+
|
|
41
|
+
if (cached !== undefined) {
|
|
42
|
+
const cachedTokenIsUsable = await deps.validateToken({
|
|
43
|
+
kandanUrl: args.kandanUrl,
|
|
44
|
+
accessToken: cached.accessToken,
|
|
45
|
+
workspaceSlug: args.workspaceSlug,
|
|
46
|
+
channelSlug: args.channelSlug,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (cachedTokenIsUsable) {
|
|
50
|
+
return cached.accessToken;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
args.reportRejectedCachedToken?.();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return await deps.acquireAndCacheToken(args);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function acquireAndCacheToken(args: LocalRunnerTokenResolutionArgs): Promise<string> {
|
|
60
|
+
const token = await acquireLocalRunnerTokenDetails({
|
|
61
|
+
kandanUrl: args.kandanUrl,
|
|
62
|
+
workspaceSlug: args.workspaceSlug,
|
|
63
|
+
channelSlug: args.channelSlug,
|
|
64
|
+
callbackHost: args.callbackHost,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
writeCachedLocalRunnerToken({
|
|
68
|
+
kandanUrl: args.kandanUrl,
|
|
69
|
+
accessToken: token.accessToken,
|
|
70
|
+
expiresInSeconds: token.expiresInSeconds,
|
|
71
|
+
authFilePath: args.authFilePath,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return token.accessToken;
|
|
75
|
+
}
|