@retasc/cli 1.39.3 → 1.39.4

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/CHANGELOG.md CHANGED
@@ -6,6 +6,30 @@ release commits and the issues they reference.
6
6
 
7
7
  Dates are the npm publish date. Each entry names the RTSC issue behind it.
8
8
 
9
+ ## 1.39.4 (2026-09-03)
10
+
11
+ - **RTSC-810** — `retasc unbind` revokes the key it says it revokes. It had two faults in
12
+ one lookup, and each on its own was enough to leave the credential live: it destructured
13
+ `{ keys }` off `listKeys`, which returns a bare array, and it derived the folder's key
14
+ prefix with a 12-character slice while the server stores 14. An exact compare between a
15
+ 12-character string and a 14-character one is never true, so every run since `unbind`
16
+ shipped (RTSC-721) printed "not found server-side (already revoked, or the org is
17
+ gone)" and moved on. The length now mirrors the server's own `displayPrefixOf`, and the
18
+ test reads that file, so the two cannot drift apart again. A session child key can no
19
+ longer be the one revoked either: those are minted in memory by the proxy and never
20
+ reach the keystore, so a match on one would mean revoking a key this folder does not own.
21
+ - **RTSC-810** — `retasc bind --org-id X --project-id Y` learns the project's prefix. The
22
+ provisioning form skipped every branch that looks a project up, so nothing knew the
23
+ prefix: the key went out nameless, the keystore entry was written without `prefix` or
24
+ `orgName`, and the receipt card printed an empty one. It now resolves the project the
25
+ same way the interactive pickers do.
26
+ - **RTSC-810** — a key is named after the folder it was bound in, not after the project.
27
+ The Keys list is the folder map (`client-a` → `ENG`), which is what tells you which
28
+ machine a credential belongs to; `ENG key` on every row told nobody anything. Naming is
29
+ the server's job now, so no door can store a nameless key: the Dash mint form with the
30
+ name left blank and `retasc key mint` without `--name` both fall back to the project's
31
+ own name rather than leaving the Dash to print "Unnamed key".
32
+
9
33
  ## 1.39.3 (2026-09-02)
10
34
 
11
35
  - **RTSC-801** — (security) `save_attachment_file` no longer reads a workspace's own secrets,
@@ -455,6 +455,20 @@ export async function completeWorkspaceSetup(args) {
455
455
  throw new Error("no project selected — pass --project-id <id>, or --project <name> --prefix <PFX>.");
456
456
  }
457
457
  }
458
+ // RTSC-810 — a `--project-id` skipped every branch above, so nothing here knew the
459
+ // project's prefix: the key went out nameless (`keyName` was built from `prefix`), the
460
+ // keystore entry was written without `prefix`/`orgName`, and the receipt card printed
461
+ // an empty one. The provisioning form is documented and supported, so it fills the
462
+ // gap the way the pickers do: one list call. Best-effort on the match — a scoped
463
+ // member may hold an id the list does not show, and that folder still has to bind.
464
+ if (projectId && !prefix) {
465
+ const { projects } = (await api.listProjects({ orgId }));
466
+ const hit = (projects ?? []).find((p) => p.id === projectId);
467
+ if (hit) {
468
+ prefix = hit.prefix;
469
+ emptyProject = projectIsEmpty(hit);
470
+ }
471
+ }
458
472
  // --- make `retasc` durable BEFORE anything is committed (RTSC-493) ---------
459
473
  // The marker names a command something else spawns on every agent start, so it has to
460
474
  // name one proved to run on this machine. Resolved (and announced) here so the install
@@ -501,7 +515,12 @@ export async function completeWorkspaceSetup(args) {
501
515
  projectId: projectId,
502
516
  agentName: opts.agent,
503
517
  runtime: opts.runtime ?? "claude-code",
504
- keyName: prefix ? `${prefix} key` : undefined,
518
+ // RTSC-810 named after the FOLDER, the leaf only, exactly as the setup-token
519
+ // door has done since RTSC-532: the Keys list is the folder map (`client-a →
520
+ // ENG`), and the old prefix-plus-"key" name told nobody which folder held it.
521
+ // The server cleans the name and falls back to the project's own when the
522
+ // leaf is empty.
523
+ keyName: basename(cwd),
505
524
  }));
506
525
  // The key is named in the receipt card below, not here — one mention, in the place
507
526
  // that says where it went (RTSC-673).
@@ -27,6 +27,35 @@ import { removeProjectMarker, tryClaudeCliRemove } from "./mcp.js";
27
27
  * • **The session is left alone.** Signing out is `logout`'s job; conflating the two
28
28
  * turns "detach this folder" into "log me out everywhere", which nobody asked.
29
29
  */
30
+ /**
31
+ * The prefix the SERVER stored for a raw key — `displayPrefixOf` in `convex/lib/keys.ts`,
32
+ * mirrored here because the CLI cannot import backend code.
33
+ *
34
+ * RTSC-810: this length is the join `unbind` revokes on, and it was wrong. The local side
35
+ * sliced 12 while the server stores 14, so the exact compare was never true once, and
36
+ * `unbind` reported "not found server-side" on every run while leaving the key LIVE —
37
+ * the same outcome as the `{ keys }` destructure below, from the other half of the same
38
+ * lookup. A change to the server's slice has to change this one; `unbindRevoke810.test.mjs`
39
+ * reads `convex/lib/keys.ts` and fails if the two ever disagree again.
40
+ */
41
+ export const DISPLAY_PREFIX_LEN = 14;
42
+ export function displayPrefixOf(key) {
43
+ return key.slice(0, DISPLAY_PREFIX_LEN);
44
+ }
45
+ /**
46
+ * This folder's live key among the org's, by display prefix — the join `unbind` revokes
47
+ * on. Takes the ARRAY `listKeys` returns; anything else is "nothing to revoke", never a
48
+ * throw, because the caller's fallback is to name the Dash rather than to fail.
49
+ *
50
+ * WORKSPACE keys only: a session child (`parentKeyId` set) is minted in memory by the
51
+ * proxy and never reaches the keystore, so one matching here would mean revoking a key
52
+ * this folder does not own.
53
+ */
54
+ export function liveKeyFor(keys, prefix) {
55
+ if (!Array.isArray(keys) || !prefix)
56
+ return undefined;
57
+ return keys.find((k) => k?.displayPrefix === prefix && !k.revokedAt && !k.parentKeyId && typeof k.id === "string");
58
+ }
30
59
  export async function unbindAction(opts) {
31
60
  const cwd = process.cwd();
32
61
  const existing = readLocalBinding(cwd);
@@ -35,7 +64,7 @@ export async function unbindAction(opts) {
35
64
  return;
36
65
  }
37
66
  const entry = existing.workspaceId ? getBinding(existing.workspaceId) : undefined;
38
- const keyPrefix = (entry?.key ?? existing.key ?? "").slice(0, 12);
67
+ const keyPrefix = displayPrefixOf(entry?.key ?? existing.key ?? "");
39
68
  console.log(`This will disconnect ${cwd} from Retasc:`);
40
69
  if (entry?.orgName || entry?.prefix) {
41
70
  console.log(` bound to: ${entry.orgName ?? entry.orgId} / ${entry.prefix ?? entry.projectId}`);
@@ -58,14 +87,18 @@ export async function unbindAction(opts) {
58
87
  return;
59
88
  }
60
89
  // Revoke FIRST, while the keystore still holds what identifies the key. displayPrefix
61
- // is how the server names keys (the raw value is never stored there), so the first 12
62
- // chars of ours is the join.
90
+ // is how the server names keys (the raw value is never stored there), so the first
91
+ // DISPLAY_PREFIX_LEN chars of ours is the join — the server's own slice, not a shorter
92
+ // guess at it (RTSC-810).
63
93
  if (entry?.orgId && keyPrefix && isLoggedIn()) {
64
94
  try {
65
- const { keys } = (await api.listKeys({ orgId: entry.orgId }));
66
- const mine = (keys ?? []).find((k) => k.displayPrefix === keyPrefix && !k.revokedAt);
95
+ // RTSC-810 `listKeys` returns a bare ARRAY (as `retasc key list` has always
96
+ // read it). This destructured `{ keys }` off it, got `undefined`, and so every
97
+ // unbind since RTSC-721 printed "not found server-side" and left the key LIVE —
98
+ // the exact half of hand-editing the command exists to stop.
99
+ const mine = liveKeyFor(await api.listKeys({ orgId: entry.orgId }), keyPrefix);
67
100
  if (mine) {
68
- await api.revokeKey({ keyId: mine.id ?? mine._id });
101
+ await api.revokeKey({ keyId: mine.id });
69
102
  console.log(`✓ Revoked key ${keyPrefix}… server-side.`);
70
103
  }
71
104
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retasc/cli",
3
- "version": "1.39.3",
3
+ "version": "1.39.4",
4
4
  "description": "Retasc CLI — the issue tracker AI agents pull work from. Sign in with GitHub or Google, create projects, mint agent API keys, and wire your agent to the Retasc MCP server in one command.",
5
5
  "type": "module",
6
6
  "bin": {