@neta-art/cohub-cli 10.0.0 → 10.1.0
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 +22 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +39 -28
- package/dist/board-kernel.js +394 -176
- package/dist/commands/run.js +14 -2
- package/dist/endpoint-options.d.ts +4 -0
- package/dist/endpoint-options.js +26 -0
- package/dist/help-path.js +2 -2
- package/dist/index.js +5 -1
- package/dist/runtime/supervisor.js +3 -3
- package/dist/space.d.ts +1 -0
- package/dist/space.js +26 -18
- package/dist/web.js +2 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -33,6 +33,28 @@ ENV=dev cohub auth login
|
|
|
33
33
|
ENV=dev cohub spaces ls
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
Use a custom API endpoint for self-hosted deployments or proxies:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cohub --base-url https://api.example.com spaces ls
|
|
40
|
+
COHUB_BASE_URL=https://api.example.com cohub spaces ls
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`--base-url` takes precedence over `COHUB_BASE_URL`. Without either, the CLI uses
|
|
44
|
+
the selected environment's default API address. Configure other endpoints separately:
|
|
45
|
+
|
|
46
|
+
| Variable | Endpoint |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `COHUB_BASE_URL` | API base URL |
|
|
49
|
+
| `COHUB_WS_URL` | Realtime gateway URL (`http(s)` or `ws(s)`) |
|
|
50
|
+
| `COHUB_WEB_URL` | Web base URL used in links |
|
|
51
|
+
|
|
52
|
+
Changing the API base URL does not change the gateway or authentication provider.
|
|
53
|
+
For self-hosted authentication, set `COHUB_AUTH_ISSUER`, `COHUB_AUTH_CLIENT_ID`,
|
|
54
|
+
and, if needed, `COHUB_AUTH_RESOURCE` and `COHUB_AUTH_SCOPE`. Stored login sessions,
|
|
55
|
+
Home Space caches, and Runtime directory bindings are scoped to the API base URL.
|
|
56
|
+
Sign in once for each custom server, or supply `COHUB_EXECUTION_TOKEN`.
|
|
57
|
+
|
|
36
58
|
## Usage
|
|
37
59
|
|
|
38
60
|
Use `--json` when reading output for decisions, extracting IDs, or chaining commands.
|
package/dist/auth.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ type DeviceCode = {
|
|
|
12
12
|
type AuthSession = {
|
|
13
13
|
schemaVersion: 1;
|
|
14
14
|
env: CohubEnvironment;
|
|
15
|
+
baseUrl?: string;
|
|
15
16
|
issuer: string;
|
|
16
17
|
clientId: string;
|
|
17
18
|
resource: string;
|
|
@@ -28,7 +29,7 @@ export declare class AuthRequiredError extends Error {
|
|
|
28
29
|
constructor(message?: string);
|
|
29
30
|
}
|
|
30
31
|
export declare const readAuthSession: () => AuthSession | null;
|
|
31
|
-
export declare const clearAuthSession: () => void;
|
|
32
|
+
export declare const clearAuthSession: (env?: CohubEnvironment, baseUrl?: string) => void;
|
|
32
33
|
export declare const authSource: () => AuthSource;
|
|
33
34
|
export declare function resolveAccessToken(options?: {
|
|
34
35
|
forceRefresh?: boolean;
|
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { resolveCohubEnvironment } from "@neta-art/cohub";
|
|
1
|
+
import { COHUB_ENVIRONMENTS, normalizeBaseUrl, resolveApiBaseUrl, resolveCohubEnvironment } from "@neta-art/cohub";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { withRuntimeSpaceBindingsLock } from "./runtime/space-binding.js";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
@@ -28,9 +28,17 @@ const authConfig = (env = currentEnv()) => {
|
|
|
28
28
|
scope: process.env[`${prefix}_AUTH_SCOPE`] ?? process.env.COHUB_AUTH_SCOPE ?? DEFAULT_SCOPE,
|
|
29
29
|
};
|
|
30
30
|
};
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
31
|
+
const endpointSuffix = (env, baseUrl = resolveApiBaseUrl({ env })) => {
|
|
32
|
+
const url = normalizeBaseUrl(baseUrl);
|
|
33
|
+
const endpoint = url === COHUB_ENVIRONMENTS[env].apiBaseUrl
|
|
34
|
+
? ""
|
|
35
|
+
: `.${createHash("sha256").update(url).digest("hex")}`;
|
|
36
|
+
return `${env === "dev" ? ".dev" : ""}${endpoint}`;
|
|
37
|
+
};
|
|
38
|
+
const sessionPath = (env = currentEnv(), baseUrl) => join(CONFIG_DIR, `auth${endpointSuffix(env, baseUrl)}.json`);
|
|
39
|
+
const deviceCodePath = (env = currentEnv(), baseUrl) => join(CONFIG_DIR, `device-code${endpointSuffix(env, baseUrl)}.json`);
|
|
40
|
+
const legacyTokenPath = (env = currentEnv(), baseUrl) => join(CONFIG_DIR, `token${endpointSuffix(env, baseUrl)}`);
|
|
41
|
+
const sessionBaseUrl = (session) => session.baseUrl ?? COHUB_ENVIRONMENTS[session.env].apiBaseUrl;
|
|
34
42
|
const writePrivateJson = (path, value) => {
|
|
35
43
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
36
44
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
@@ -106,6 +114,7 @@ const toSession = (token, config, env, previous) => {
|
|
|
106
114
|
return {
|
|
107
115
|
schemaVersion: 1,
|
|
108
116
|
env,
|
|
117
|
+
baseUrl: previous ? sessionBaseUrl(previous) : resolveApiBaseUrl({ env }),
|
|
109
118
|
issuer: config.issuer,
|
|
110
119
|
clientId: config.clientId,
|
|
111
120
|
resource: config.resource,
|
|
@@ -120,13 +129,10 @@ const toSession = (token, config, env, previous) => {
|
|
|
120
129
|
};
|
|
121
130
|
};
|
|
122
131
|
export const readAuthSession = () => readJson(sessionPath());
|
|
123
|
-
const
|
|
124
|
-
removeIfExists(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
removeIfExists(sessionPath());
|
|
128
|
-
clearDeviceCode();
|
|
129
|
-
removeIfExists(legacyTokenPath());
|
|
132
|
+
export const clearAuthSession = (env = currentEnv(), baseUrl = resolveApiBaseUrl({ env })) => {
|
|
133
|
+
removeIfExists(sessionPath(env, baseUrl));
|
|
134
|
+
removeIfExists(deviceCodePath(env, baseUrl));
|
|
135
|
+
removeIfExists(legacyTokenPath(env, baseUrl));
|
|
130
136
|
};
|
|
131
137
|
export const authSource = () => {
|
|
132
138
|
if (process.env.COHUB_EXECUTION_TOKEN?.trim())
|
|
@@ -152,14 +158,15 @@ export async function requireAccessToken() {
|
|
|
152
158
|
throw new AuthRequiredError();
|
|
153
159
|
return token;
|
|
154
160
|
}
|
|
155
|
-
|
|
161
|
+
const refreshesInFlight = new Map();
|
|
156
162
|
export function refreshAccessToken(session = readAuthSession()) {
|
|
157
163
|
if (!session?.refreshToken)
|
|
158
164
|
return Promise.reject(new AuthRequiredError());
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
const path = sessionPath(session.env, sessionBaseUrl(session));
|
|
166
|
+
const inFlight = refreshesInFlight.get(path);
|
|
167
|
+
if (inFlight)
|
|
168
|
+
return inFlight;
|
|
169
|
+
const refresh = withRuntimeSpaceBindingsLock(async () => {
|
|
163
170
|
const current = readJson(path);
|
|
164
171
|
if (!current)
|
|
165
172
|
throw new AuthRequiredError();
|
|
@@ -167,11 +174,13 @@ export function refreshAccessToken(session = readAuthSession()) {
|
|
|
167
174
|
if (current.refreshToken !== session.refreshToken || current.updatedAt !== session.updatedAt)
|
|
168
175
|
return current.accessToken;
|
|
169
176
|
return performRefresh(current);
|
|
170
|
-
}, { path, lockPath: `${path}.refresh.lock` }).finally(() => {
|
|
171
|
-
|
|
177
|
+
}, { path, lockPath: `${path}.refresh.lock` }).finally(() => { refreshesInFlight.delete(path); });
|
|
178
|
+
refreshesInFlight.set(path, refresh);
|
|
179
|
+
return refresh;
|
|
172
180
|
}
|
|
173
181
|
async function performRefresh(session) {
|
|
174
182
|
const config = authConfig(session.env);
|
|
183
|
+
const path = sessionPath(session.env, sessionBaseUrl(session));
|
|
175
184
|
const { response, data } = await formPost(tokenEndpoint(session.issuer), new URLSearchParams({
|
|
176
185
|
client_id: session.clientId,
|
|
177
186
|
grant_type: "refresh_token",
|
|
@@ -181,24 +190,25 @@ async function performRefresh(session) {
|
|
|
181
190
|
}));
|
|
182
191
|
if (!response.ok) {
|
|
183
192
|
if (isUnrecoverableRefreshError(data)) {
|
|
184
|
-
if (readJson(
|
|
185
|
-
clearAuthSession();
|
|
193
|
+
if (readJson(path)?.refreshToken === session.refreshToken)
|
|
194
|
+
clearAuthSession(session.env, sessionBaseUrl(session));
|
|
186
195
|
return null;
|
|
187
196
|
}
|
|
188
197
|
throw new Error(formatAuthError(data, response.status));
|
|
189
198
|
}
|
|
190
199
|
const next = toSession(data, config, session.env, session);
|
|
191
200
|
// Never overwrite a login/logout that completed while the request was in flight.
|
|
192
|
-
const current = readJson(
|
|
201
|
+
const current = readJson(path);
|
|
193
202
|
if (!current)
|
|
194
203
|
throw new AuthRequiredError();
|
|
195
204
|
if (current.refreshToken !== session.refreshToken)
|
|
196
205
|
return current.accessToken;
|
|
197
|
-
writePrivateJson(
|
|
206
|
+
writePrivateJson(path, next);
|
|
198
207
|
return next.accessToken;
|
|
199
208
|
}
|
|
200
209
|
export async function requestDeviceCode() {
|
|
201
210
|
const env = currentEnv();
|
|
211
|
+
const baseUrl = resolveApiBaseUrl({ env });
|
|
202
212
|
const config = authConfig(env);
|
|
203
213
|
const { response, data } = await formPost(deviceEndpoint(config.issuer), new URLSearchParams({
|
|
204
214
|
client_id: config.clientId,
|
|
@@ -224,13 +234,14 @@ export async function requestDeviceCode() {
|
|
|
224
234
|
interval,
|
|
225
235
|
createdAt: now,
|
|
226
236
|
};
|
|
227
|
-
writePrivateJson(deviceCodePath(env), deviceCode);
|
|
237
|
+
writePrivateJson(deviceCodePath(env, baseUrl), deviceCode);
|
|
228
238
|
return deviceCode;
|
|
229
239
|
}
|
|
230
240
|
export async function verifyDeviceCode() {
|
|
231
241
|
const env = currentEnv();
|
|
242
|
+
const baseUrl = resolveApiBaseUrl({ env });
|
|
232
243
|
const config = authConfig(env);
|
|
233
|
-
const deviceCode = readJson(deviceCodePath(env));
|
|
244
|
+
const deviceCode = readJson(deviceCodePath(env, baseUrl));
|
|
234
245
|
if (!deviceCode)
|
|
235
246
|
throw new Error("Device code not found. Run `cohub auth login --request-code` first.");
|
|
236
247
|
if (deviceCode.expiresAt <= Date.now())
|
|
@@ -243,9 +254,9 @@ export async function verifyDeviceCode() {
|
|
|
243
254
|
}));
|
|
244
255
|
if (!response.ok)
|
|
245
256
|
throw new Error(formatAuthError(data, response.status));
|
|
246
|
-
const session = toSession(data, config, env);
|
|
247
|
-
writePrivateJson(sessionPath(env), session);
|
|
248
|
-
removeIfExists(deviceCodePath(env));
|
|
257
|
+
const session = { ...toSession(data, config, env), baseUrl };
|
|
258
|
+
writePrivateJson(sessionPath(env, sessionBaseUrl(session)), session);
|
|
259
|
+
removeIfExists(deviceCodePath(env, baseUrl));
|
|
249
260
|
return session;
|
|
250
261
|
}
|
|
251
262
|
export async function loginWithDeviceFlow(onCode, onPoll) {
|