@levr-one/cli 0.12.4 → 0.12.6

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.
Files changed (23) hide show
  1. package/dist/{addHandler-LO6SChns.js → addHandler-ybX54_3f.js} +6 -6
  2. package/dist/{appendHandler-DtxLpPu6.js → appendHandler-CKkl9ymw.js} +7 -7
  3. package/dist/{artifact-upload-BvBSevIx.js → artifact-upload-Ba801tb6.js} +1 -1
  4. package/dist/{attachHandler-DXZi2ILT.js → attachHandler-CfNvVAV2.js} +7 -7
  5. package/dist/cli.js +77 -18
  6. package/dist/{connect-DVjgyfls.js → connect-yq3fGXkW.js} +9 -8
  7. package/dist/{find-workspace-Bl4rfr73.js → find-workspace-4Us2vEb7.js} +1 -1
  8. package/dist/{importHandler-uFdalKtw.js → importHandler-bYBWYC_0.js} +8 -8
  9. package/dist/{listHandler-BW92JLCl.js → listHandler-Du_YGzre.js} +4 -4
  10. package/dist/{loginHandler-C9Oi92IY.js → loginHandler-PbKmDAGo.js} +4 -4
  11. package/dist/{logoutHandler-Cf5aI1wq.js → logoutHandler-DxnzGPYx.js} +1 -1
  12. package/dist/{pushHandler-CXFZt3SO.js → pushHandler-BulraEwP.js} +15 -13
  13. package/dist/refsHandler-DtcceTRP.js +1757 -0
  14. package/dist/{resolve-token-ByZq05Us.js → resolve-token-DKEXT7SA.js} +8 -5
  15. package/dist/{resolve-workspace-BbK3uYQc.js → resolve-workspace-D8y7twod.js} +37 -5
  16. package/dist/{runHandler-IyPhhiGs.js → runHandler-B-u-0eRk.js} +25 -13
  17. package/dist/{sdk-client-B4FMd_Fa.js → sdk-client-BgzKzVIR.js} +1 -1
  18. package/dist/{selectHandler-D6cvq60S.js → selectHandler-C0gs-Q70.js} +9 -4
  19. package/dist/{statusHandler-BQXn93-r.js → statusHandler-Dnnh80ZN.js} +7 -5
  20. package/dist/{token-refresh-CJB7Qlxy.js → token-refresh-BpFuU4Ij.js} +26 -2
  21. package/package.json +1 -1
  22. /package/dist/{currentHandler-QhCywr3S.js → currentHandler-PaLZKLGF.js} +0 -0
  23. /package/dist/{env-CvjMSRd7.js → env-DjeRwmZ7.js} +0 -0
@@ -1,5 +1,5 @@
1
- import { getApiUrl, getPatToken, readCredentials } from "./env-CvjMSRd7.js";
2
- import { isTokenExpired, refreshToken } from "./token-refresh-CJB7Qlxy.js";
1
+ import { classifyToken, isTokenExpired, refreshToken } from "./token-refresh-BpFuU4Ij.js";
2
+ import { getApiUrl, getPatToken, readCredentials } from "./env-DjeRwmZ7.js";
3
3
 
4
4
  //#region src/auth/resolve-token.ts
5
5
  /**
@@ -16,7 +16,8 @@ var CredentialsMismatchError = class extends Error {
16
16
  };
17
17
  /**
18
18
  * Resolve auth token in priority order:
19
- * 1. LEVR_TOKEN env var (PAT) — long-lived, no refresh
19
+ * 1. LEVR_TOKEN env var — a PAT or an OAuth access token, told apart by
20
+ * shape (ENG-6068); never refreshed
20
21
  * 2. Stored credentials (JWT) — auto-refresh if expired
21
22
  * 3. Error — not authenticated
22
23
  */
@@ -24,7 +25,8 @@ async function resolveToken() {
24
25
  const pat = getPatToken();
25
26
  if (pat) return {
26
27
  token: pat,
27
- type: "pat"
28
+ type: classifyToken(pat),
29
+ source: "env"
28
30
  };
29
31
  let creds = readCredentials();
30
32
  if (creds) {
@@ -38,7 +40,8 @@ async function resolveToken() {
38
40
  }
39
41
  return {
40
42
  token: creds.access_token,
41
- type: "jwt"
43
+ type: "jwt",
44
+ source: "stored"
42
45
  };
43
46
  }
44
47
  throw new Error("Not authenticated. Run 'levr auth login' or set LEVR_TOKEN environment variable.");
@@ -1,4 +1,4 @@
1
- import { authGetSitesV1 } from "./sdk-client-B4FMd_Fa.js";
1
+ import { authGetSitesV1 } from "./sdk-client-BgzKzVIR.js";
2
2
  import { clearWorkspace, loadWorkspace, saveWorkspace } from "./workspace-store-Bmw104Qa.js";
3
3
 
4
4
  //#region src/workspace/resolve-workspace.ts
@@ -40,7 +40,8 @@ async function autoSelectWorkspace() {
40
40
  };
41
41
  return { kind: "none" };
42
42
  }
43
- async function resolveWorkspace(flagValue) {
43
+ async function resolveWorkspace(flagValue, options = {}) {
44
+ const persist = options.persist ?? true;
44
45
  let sites = null;
45
46
  const getSites = async () => {
46
47
  if (!sites) sites = await fetchSites();
@@ -61,7 +62,7 @@ async function resolveWorkspace(flagValue) {
61
62
  source: "env"
62
63
  };
63
64
  }
64
- const cached = loadWorkspace();
65
+ const cached = persist ? loadWorkspace() : null;
65
66
  if (cached) {
66
67
  if ((await getSites()).some((site) => site.workspace_id === cached)) return {
67
68
  workspaceId: cached,
@@ -73,7 +74,7 @@ async function resolveWorkspace(flagValue) {
73
74
  if (s.length === 0) throw new Error("No workspaces available.");
74
75
  if (s.length === 1) {
75
76
  const single = s[0];
76
- saveWorkspace(single.workspace_id);
77
+ if (persist) saveWorkspace(single.workspace_id);
77
78
  return {
78
79
  workspaceId: single.workspace_id,
79
80
  source: "auto"
@@ -84,6 +85,37 @@ async function resolveWorkspace(flagValue) {
84
85
  const overflow = s.length > maxList ? `\n ... and ${s.length - maxList} more` : "";
85
86
  throw new Error(`Multiple workspaces. Select one:\n${list}${overflow}\n\nRun: levr workspace select <id>`);
86
87
  }
88
+ /**
89
+ * The workspace a command runs in, for whichever credential it holds —
90
+ * [issue:ENG-6068].
91
+ *
92
+ * A JWT is not bound to a workspace, so one is resolved the usual way: the
93
+ * flag, `LEVR_WORKSPACE_ID`, the cached selection, then auto-select, each
94
+ * validated against the sites the account can see.
95
+ *
96
+ * A personal access token IS bound to one workspace, and the server scopes
97
+ * every request by it. An explicit `--workspace-id` or `LEVR_WORKSPACE_ID` is
98
+ * still sent as `Workspace-Id` — the server refuses one that is not the
99
+ * token's — but nothing is looked up or cached, and with neither set the
100
+ * answer is null: the token's own workspace applies. Until ENG-6068 both were
101
+ * silently dropped for every `LEVR_TOKEN`, including an OAuth access token,
102
+ * which was classified as a PAT by where it came from and has no workspace to
103
+ * fall back to. An OAuth token from LEVR_TOKEN is not the login session, so
104
+ * the saved workspace choice is left alone for it (`persist: false`).
105
+ */
106
+ async function resolveWorkspaceFor(auth, flagValue) {
107
+ if (auth.type === "jwt") return resolveWorkspace(flagValue, { persist: auth.source !== "env" });
108
+ if (flagValue) return {
109
+ workspaceId: flagValue,
110
+ source: "flag"
111
+ };
112
+ const envWs = process.env["LEVR_WORKSPACE_ID"];
113
+ if (envWs) return {
114
+ workspaceId: envWs,
115
+ source: "env"
116
+ };
117
+ return null;
118
+ }
87
119
 
88
120
  //#endregion
89
- export { autoSelectWorkspace, fetchSites, resolveWorkspace };
121
+ export { autoSelectWorkspace, fetchSites, resolveWorkspaceFor };
@@ -1,10 +1,10 @@
1
1
  import { executeInspect } from "./inspect-BbL7kDe-.js";
2
- import { client, configureClient } from "./sdk-client-B4FMd_Fa.js";
3
- import "./env-CvjMSRd7.js";
4
- import "./token-refresh-CJB7Qlxy.js";
5
- import { resolveToken } from "./resolve-token-ByZq05Us.js";
2
+ import { client, configureClient } from "./sdk-client-BgzKzVIR.js";
3
+ import "./token-refresh-BpFuU4Ij.js";
4
+ import "./env-DjeRwmZ7.js";
5
+ import { resolveToken } from "./resolve-token-DKEXT7SA.js";
6
6
  import "./workspace-store-Bmw104Qa.js";
7
- import { resolveWorkspace } from "./resolve-workspace-BbK3uYQc.js";
7
+ import { resolveWorkspaceFor } from "./resolve-workspace-D8y7twod.js";
8
8
  import { existsSync } from "node:fs";
9
9
  import { constants as constants$1 } from "node:os";
10
10
  import { spawnSync } from "node:child_process";
@@ -365,6 +365,15 @@ function delay(ms) {
365
365
 
366
366
  //#endregion
367
367
  //#region src/commands/gates/runHandler.ts
368
+ /** The server's own words for a failed SDK call, for an error line. */
369
+ function describeSdkError(error) {
370
+ if (typeof error === "string") return error.length > 200 ? `${error.slice(0, 200)}…` : error;
371
+ if (!error || typeof error !== "object") return "request failed";
372
+ const e = error;
373
+ const message = Array.isArray(e.message) ? e.message.map(String).join("; ") : typeof e.message === "string" ? e.message : typeof e.error === "string" ? e.error : "";
374
+ const text = [typeof e.statusCode === "number" ? `HTTP ${e.statusCode}` : "", message].filter(Boolean).join(": ") || "request failed";
375
+ return text.length > 200 ? `${text.slice(0, 200)}…` : text;
376
+ }
368
377
  /** ENG-6070 review R5 M3 — dead gates are named, with their remedy. */
369
378
  function printDeadGates(out, dead) {
370
379
  for (const d of dead ?? []) out(` DEAD ${d.test_name ? `"${d.test_name}"` : d.link_id}${d.required === false ? " (advisory)" : ""}: ${d.reason === "test_deleted" ? "its test was deleted" : "the gate was removed"}${d.remedy ? ` — ${d.remedy}` : ""}`);
@@ -431,13 +440,15 @@ async function gatesRunHandler(flags, issue) {
431
440
  };
432
441
  if (flags.ci) setCiHeaders(null);
433
442
  let workspaceId;
434
- if (auth.type === "jwt") try {
435
- const ws = await resolveWorkspace(flags["workspace-id"]);
436
- workspaceId = ws.workspaceId;
437
- client.setConfig({
438
- ...client.getConfig(),
439
- workspaceId: ws.workspaceId
440
- });
443
+ try {
444
+ const ws = await resolveWorkspaceFor(auth, flags["workspace-id"]);
445
+ if (ws) {
446
+ workspaceId = ws.workspaceId;
447
+ client.setConfig({
448
+ ...client.getConfig(),
449
+ workspaceId: ws.workspaceId
450
+ });
451
+ }
441
452
  } catch (err) {
442
453
  this.logger.error(err instanceof Error ? err.message : "Workspace resolution failed.");
443
454
  this.process.exitCode = 1;
@@ -451,7 +462,8 @@ async function gatesRunHandler(flags, issue) {
451
462
  } });
452
463
  const row = found.data?.data?.[0];
453
464
  if (found.error || !row) {
454
- this.logger.error(`Issue ${issue} not found in this workspace.`);
465
+ const where = workspaceId ? `workspace ${workspaceId}` : "the token's workspace";
466
+ this.logger.error(found.error ? `Could not look up issue ${issue} in ${where} (${describeSdkError(found.error)}).` : `Issue ${issue} not found in ${where}.`);
455
467
  this.process.exitCode = 1;
456
468
  return;
457
469
  }
@@ -1,4 +1,4 @@
1
- import { getApiUrl } from "./env-CvjMSRd7.js";
1
+ import { getApiUrl } from "./env-DjeRwmZ7.js";
2
2
 
3
3
  //#region ../sdk-public/dist/runtime/client.js
4
4
  function createInterceptorManager() {
@@ -1,7 +1,7 @@
1
- import { authGetSitesV1, configureClient } from "./sdk-client-B4FMd_Fa.js";
2
- import "./env-CvjMSRd7.js";
3
- import "./token-refresh-CJB7Qlxy.js";
4
- import { resolveToken } from "./resolve-token-ByZq05Us.js";
1
+ import { authGetSitesV1, configureClient } from "./sdk-client-BgzKzVIR.js";
2
+ import "./token-refresh-BpFuU4Ij.js";
3
+ import "./env-DjeRwmZ7.js";
4
+ import { resolveToken } from "./resolve-token-DKEXT7SA.js";
5
5
  import { saveWorkspace } from "./workspace-store-Bmw104Qa.js";
6
6
 
7
7
  //#region src/commands/workspace/selectHandler.ts
@@ -19,6 +19,11 @@ async function selectHandler(_flags, workspaceId) {
19
19
  this.process.exitCode = 1;
20
20
  return;
21
21
  }
22
+ if (auth.source === "env") {
23
+ this.logger.error("LEVR_TOKEN is set: a workspace selected here would be saved for your login session, not for that token. Pass --workspace-id or set LEVR_WORKSPACE_ID instead.");
24
+ this.process.exitCode = 1;
25
+ return;
26
+ }
22
27
  configureClient(auth);
23
28
  const result = await authGetSitesV1();
24
29
  if (result.error) {
@@ -1,6 +1,6 @@
1
- import { authGetProfileV1, configureClient } from "./sdk-client-B4FMd_Fa.js";
2
- import { getApiUrl, getPatToken, readCredentials } from "./env-CvjMSRd7.js";
3
- import { isTokenExpired } from "./token-refresh-CJB7Qlxy.js";
1
+ import { authGetProfileV1, configureClient } from "./sdk-client-BgzKzVIR.js";
2
+ import { classifyToken, isTokenExpired } from "./token-refresh-BpFuU4Ij.js";
3
+ import { getApiUrl, getPatToken, readCredentials } from "./env-DjeRwmZ7.js";
4
4
  import chalk from "chalk";
5
5
 
6
6
  //#region src/commands/auth/statusHandler.ts
@@ -8,8 +8,10 @@ async function statusHandler() {
8
8
  const apiUrl = getApiUrl();
9
9
  const pat = getPatToken();
10
10
  if (pat) {
11
- if (await checkApiReachable(pat, "pat")) {
12
- this.process.stdout.write(`${chalk.green("ok")} Authenticated via LEVR_TOKEN (PAT)\n`);
11
+ const type = classifyToken(pat);
12
+ const kind = type === "pat" ? "personal access token" : "OAuth access token";
13
+ if (await checkApiReachable(pat, type)) {
14
+ this.process.stdout.write(`${chalk.green("ok")} Authenticated via LEVR_TOKEN (${kind})\n`);
13
15
  this.process.stdout.write(` API: ${apiUrl} (reachable)\n`);
14
16
  } else {
15
17
  this.process.stdout.write(`${chalk.yellow("warn")} LEVR_TOKEN is set but API is unreachable\n`);
@@ -1,5 +1,29 @@
1
- import { CLI_CLIENT_ID, deleteCredentials, getApiUrl, writeCredentials } from "./env-CvjMSRd7.js";
1
+ import { CLI_CLIENT_ID, deleteCredentials, getApiUrl, writeCredentials } from "./env-DjeRwmZ7.js";
2
2
 
3
+ //#region src/auth/token-shape.ts
4
+ /**
5
+ * Classify a bearer token by its SHAPE — [issue:ENG-6068].
6
+ *
7
+ * The server decides how to verify a token by looking at the token, not at
8
+ * where it came from (`isPersonalAccessToken` in the backend's auth
9
+ * middleware): a Levr personal access token starts with `tq_`, and anything
10
+ * else is handed to the JWT verifier. `LEVR_TOKEN` carries either kind — a
11
+ * PAT minted in Settings, or an OAuth access token a device-code login
12
+ * printed for CI — so the CLI must apply the same rule. Until ENG-6068 every
13
+ * `LEVR_TOKEN` was called a PAT, and the JWT-only steps (resolving
14
+ * `--workspace-id` / `LEVR_WORKSPACE_ID`, `levr workspace list|select`) were
15
+ * silently skipped for an OAuth token, which has no workspace of its own.
16
+ *
17
+ * Leaf module on purpose: no imports, so `resolve-token`, the handlers and
18
+ * their tests can all read it without pulling in credentials or the SDK.
19
+ */
20
+ const PAT_PREFIX = "tq_";
21
+ /** `tq_…` is a personal access token; everything else is treated as a JWT. */
22
+ function classifyToken(token) {
23
+ return token.startsWith(PAT_PREFIX) ? "pat" : "jwt";
24
+ }
25
+
26
+ //#endregion
3
27
  //#region src/auth/token-refresh.ts
4
28
  const REFRESH_BUFFER_MS = 300 * 1e3;
5
29
  /**
@@ -45,4 +69,4 @@ async function refreshToken(creds) {
45
69
  }
46
70
 
47
71
  //#endregion
48
- export { isTokenExpired, refreshToken };
72
+ export { classifyToken, isTokenExpired, refreshToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levr-one/cli",
3
- "version": "0.12.4",
3
+ "version": "0.12.6",
4
4
  "description": "The command-line interface for Levr",
5
5
  "type": "module",
6
6
  "license": "MIT",
File without changes