@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 +24 -0
- package/dist/commands/bind.js +20 -1
- package/dist/commands/unbind.js +39 -6
- package/package.json +1 -1
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,
|
package/dist/commands/bind.js
CHANGED
|
@@ -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
|
-
|
|
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).
|
package/dist/commands/unbind.js
CHANGED
|
@@ -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 ?? "")
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
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
|
|
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
|
+
"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": {
|