impel-cli 0.9.0 → 0.9.1

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 CHANGED
@@ -124,6 +124,7 @@ impel auth [--pat <pat>] [--gateway <url>] [--app <url>]
124
124
  impel pat create --label <label> [options] Mint a personal PAT through the control plane
125
125
  impel pat create --agent <agent-id> [options]
126
126
  Mint a PAT associated with an org agent
127
+ impel pat revoke <token-id> --yes Irreversibly revoke one of your PATs
127
128
  impel token [--tenant <org>] Print a selected-tenant bearer
128
129
  impel mcp Run the local MCP bridge used by Claude/Codex
129
130
  impel claude [claude args...] Launch isolated Impel Claude Code
@@ -186,7 +187,7 @@ Stores your PAT, gateway base URL, and app/control-plane URL in `~/.config/impel
186
187
  impel auth --pat impel_pat_xxxxxxxxxxxx --gateway https://gateway.useimpel.com --app https://www.useimpel.com
187
188
  ```
188
189
 
189
- ### `impel pat create ...` (alias `impel pat mint ...`)
190
+ ### `impel pat create ...` / `impel pat revoke ...`
190
191
 
191
192
  Mints a new PAT through the app/control-plane API, authenticated by the PAT
192
193
  already stored by `impel auth`. Identity remains the minting authority: the
@@ -215,6 +216,20 @@ the config file. To rotate this CLI onto it, explicitly run
215
216
  `impel auth --pat <new-pat>` after copying it. `--json` includes the raw secret,
216
217
  so treat redirected output as a credential.
217
218
 
219
+ Revoke a token by the token ID printed when it was created:
220
+
221
+ ```sh
222
+ impel pat revoke personal1 --yes
223
+ impel pat revoke personal1 --yes --json
224
+ ```
225
+
226
+ Revocation is immediate and irreversible, so `--yes` is required. A full PAT is
227
+ also accepted for recovery from older mint output, but using the token ID avoids
228
+ putting a secret in shell history. The CLI extracts the ID without echoing the
229
+ secret, authenticates with the currently stored PAT, and leaves the local config
230
+ unchanged. If you revoke the stored PAT itself, authenticate again with a live PAT
231
+ before running another protected command.
232
+
218
233
  ### `impel update`
219
234
 
220
235
  One command brings everything current, in dependency order: the CLI itself
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Configure Claude Code and Codex CLI to talk to Impel's gateway, authenticated by an Impel Personal Access Token",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -37,6 +37,7 @@ Manage:
37
37
  impel auth [--pat <pat>] Store your PAT + URLs (--gateway/--app <url>)
38
38
  impel pat create --label <label> Mint a personal PAT (--org/--scopes/--ttl-days)
39
39
  impel pat create --agent <agent-id> Mint an org-scoped PAT associated with an agent
40
+ impel pat revoke <token-id> --yes Irreversibly revoke one of your PATs
40
41
  impel tenant list|current|use <org> List or select the active organization
41
42
  impel tasks list|get|create|update|delete CRUD Impel tickets (aliases: task, ticket[s])
42
43
  impel token [--tenant <org>] Print the selected-tenant bearer
@@ -16,8 +16,9 @@ import {
16
16
  const AGENT_PAT_LABEL_PREFIX = "Agent · ";
17
17
  const MAX_PAT_LABEL_LENGTH = 120;
18
18
  const MAX_PAT_TTL_DAYS = 365;
19
- const PAT_CREATE_PATH = "/api/cli/pats";
19
+ const PAT_COLLECTION_PATH = "/api/cli/pats";
20
20
  const PAT_TOKEN_RE = /^impel_pat_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/u;
21
+ const PAT_TOKEN_INPUT_RE = /^impel_pat_([A-Za-z0-9_-]{8,128})\.[A-Za-z0-9_-]+$/u;
21
22
  const PAT_TOKEN_ID_RE = /^[A-Za-z0-9_-]{8,128}$/u;
22
23
  const AGENT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,111}$/u;
23
24
  const CONTROL_CHARACTER_RE = /[\u0000-\u001F\u007F-\u009F]/u;
@@ -33,13 +34,28 @@ const FLAG_NAMES = new Set([
33
34
  "scopes",
34
35
  "tenant",
35
36
  "ttl-days",
37
+ "yes",
36
38
  ]);
37
39
 
38
- const HELP = `impel pat - mint a personal access token through Impel
40
+ const CREATE_FLAG_NAMES = new Set([
41
+ "agent",
42
+ "app",
43
+ "help",
44
+ "json",
45
+ "label",
46
+ "org",
47
+ "scopes",
48
+ "tenant",
49
+ "ttl-days",
50
+ ]);
51
+ const REVOKE_FLAG_NAMES = new Set(["app", "help", "json", "yes"]);
52
+
53
+ const HELP = `impel pat - manage personal access tokens through Impel
39
54
 
40
55
  Usage:
41
56
  impel pat create --label <label> [options]
42
57
  impel pat create --agent <agent-id> [options]
58
+ impel pat revoke <token-id> --yes [options]
43
59
 
44
60
  Aliases:
45
61
  impel pats ...
@@ -52,11 +68,13 @@ Options:
52
68
  Defaults are chosen by the control plane for your access tier.
53
69
  --ttl-days <days> Expiry from 1 to ${MAX_PAT_TTL_DAYS} days. Default: 90.
54
70
  --app <url> Override the app/control-plane URL for this command.
55
- --json Print the created PAT and metadata as JSON.
71
+ --json Print the result as JSON.
72
+ --yes Confirm irreversible PAT revocation.
56
73
 
57
74
  The stored PAT authenticates this request. The control plane verifies the owner,
58
75
  organization membership, requested scopes, and agent access before identity mints
59
- the new secret. The new PAT is shown once and is not stored automatically.
76
+ the new secret or revokes the selected token. New PATs are shown once and are not
77
+ stored automatically. Revoke accepts a token id or a full PAT and never echoes a secret.
60
78
  `;
61
79
 
62
80
  class PatCommandError extends Error {}
@@ -76,9 +94,17 @@ function flagSpec() {
76
94
  scopes: { type: "string" },
77
95
  tenant: { type: "string" },
78
96
  "ttl-days": { type: "string" },
97
+ yes: { type: "boolean" },
79
98
  };
80
99
  }
81
100
 
101
+ function rejectUnsupportedFlags(flags, allowed, action) {
102
+ const unsupported = Object.keys(flags).find((name) => !allowed.has(name));
103
+ if (unsupported) {
104
+ fail(`impel pat ${action}: option --${redactSecretText(unsupported)} is not supported.`);
105
+ }
106
+ }
107
+
82
108
  function requireConfig(flags) {
83
109
  const config = loadConfig();
84
110
  if (!config?.pat) {
@@ -135,6 +161,18 @@ function requestedTtlDays(value) {
135
161
  return days;
136
162
  }
137
163
 
164
+ function requestedTokenId(value) {
165
+ if (value === undefined) {
166
+ fail("impel pat revoke: missing token id.");
167
+ }
168
+ const requested = String(value).trim();
169
+ const tokenId = PAT_TOKEN_INPUT_RE.exec(requested)?.[1] || requested;
170
+ if (!PAT_TOKEN_ID_RE.test(tokenId)) {
171
+ fail("impel pat revoke: expected a valid token id or full Impel PAT.");
172
+ }
173
+ return tokenId;
174
+ }
175
+
138
176
  function requestedSubject(flags) {
139
177
  const agentId = flags.agent === undefined ? null : String(flags.agent).trim();
140
178
  const personalLabel = flags.label === undefined ? null : String(flags.label).trim();
@@ -215,19 +253,19 @@ function normalizeCreatedPat(payload, expected) {
215
253
  };
216
254
  }
217
255
 
218
- async function createPat({ appUrl, currentPat, body }) {
256
+ async function requestControlPlane({ appUrl, currentPat, path, method, body, feature }) {
219
257
  const controller = new AbortController();
220
258
  const timeout = setTimeout(() => controller.abort(), 10_000);
221
259
  let response;
222
260
  try {
223
- response = await fetch(new URL(PAT_CREATE_PATH, appUrl), {
224
- method: "POST",
261
+ response = await fetch(new URL(path, appUrl), {
262
+ method,
225
263
  headers: {
226
264
  accept: "application/json",
227
265
  authorization: `Bearer ${currentPat}`,
228
- "content-type": "application/json",
266
+ ...(body === undefined ? {} : { "content-type": "application/json" }),
229
267
  },
230
- body: JSON.stringify(body),
268
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
231
269
  signal: controller.signal,
232
270
  });
233
271
  } catch (error) {
@@ -245,13 +283,24 @@ async function createPat({ appUrl, currentPat, body }) {
245
283
  payload = null;
246
284
  }
247
285
  if (!response.ok) {
248
- if (response.status === 404) {
249
- fail(`impel pat: PAT minting is not available on ${appUrl}; update the Impel control plane and try again.`);
286
+ if (response.status === 404 && !payload?.error) {
287
+ fail(`impel pat: PAT ${feature} is not available on ${appUrl}; update the Impel control plane and try again.`);
250
288
  }
251
289
  const message = redactSecretText(payload?.error || `${response.status} ${response.statusText}`.trim());
252
290
  fail(`impel pat: ${message}`);
253
291
  }
292
+ return payload;
293
+ }
254
294
 
295
+ async function createPat({ appUrl, currentPat, body }) {
296
+ const payload = await requestControlPlane({
297
+ appUrl,
298
+ currentPat,
299
+ path: PAT_COLLECTION_PATH,
300
+ method: "POST",
301
+ body,
302
+ feature: "minting",
303
+ });
255
304
  const created = normalizeCreatedPat(payload, body);
256
305
  if (!created) {
257
306
  fail("impel pat: the control plane returned an invalid PAT creation response; the secret was not displayed.");
@@ -259,6 +308,17 @@ async function createPat({ appUrl, currentPat, body }) {
259
308
  return created;
260
309
  }
261
310
 
311
+ async function revokePat({ appUrl, currentPat, tokenId }) {
312
+ await requestControlPlane({
313
+ appUrl,
314
+ currentPat,
315
+ path: `${PAT_COLLECTION_PATH}/${encodeURIComponent(tokenId)}`,
316
+ method: "DELETE",
317
+ feature: "revocation",
318
+ });
319
+ return { revoked: true, tokenId };
320
+ }
321
+
262
322
  function printCreatedPat(created, subject, json) {
263
323
  if (json) {
264
324
  console.log(JSON.stringify({
@@ -274,6 +334,7 @@ function printCreatedPat(created, subject, json) {
274
334
  } else {
275
335
  console.log(`Created personal PAT ${JSON.stringify(created.label)} in ${created.orgId}.`);
276
336
  }
337
+ console.log(`Token ID: ${created.tokenId}`);
277
338
  console.log(`Scopes: ${created.scopes.join(", ") || "none"}`);
278
339
  console.log(`Expires: ${created.expiresAt === null ? "never" : new Date(created.expiresAt).toISOString()}`);
279
340
  console.log("");
@@ -281,15 +342,23 @@ function printCreatedPat(created, subject, json) {
281
342
  console.log(created.token);
282
343
  }
283
344
 
345
+ function printRevokedPat(revoked, json) {
346
+ if (json) {
347
+ console.log(JSON.stringify(revoked, null, 2));
348
+ return;
349
+ }
350
+ console.log(`Revoked PAT ${revoked.tokenId}.`);
351
+ }
352
+
284
353
  export async function cmdPat(argv) {
285
354
  const [action, ...rest] = argv;
286
355
  if (!action || action === "help" || action === "--help" || action === "-h") {
287
356
  console.log(HELP);
288
357
  return;
289
358
  }
290
- if (action !== "create" && action !== "mint") {
359
+ if (action !== "create" && action !== "mint" && action !== "revoke") {
291
360
  console.error(`impel pat: unknown subcommand "${redactSecretText(action)}".`);
292
- console.error(" Use `impel pat create --label <label>` or `impel pat create --agent <agent-id>`.");
361
+ console.error(" Use `impel pat create --label <label>`, `impel pat create --agent <agent-id>`, or `impel pat revoke <token-id> --yes`.");
293
362
  process.exitCode = 1;
294
363
  return;
295
364
  }
@@ -302,6 +371,23 @@ export async function cmdPat(argv) {
302
371
  }
303
372
  const unknownFlag = Object.keys(flags).find((name) => !FLAG_NAMES.has(name));
304
373
  if (unknownFlag) fail(`impel pat: unknown option --${redactSecretText(unknownFlag)}.`);
374
+
375
+ if (action === "revoke") {
376
+ rejectUnsupportedFlags(flags, REVOKE_FLAG_NAMES, "revoke");
377
+ const tokenId = requestedTokenId(positionals[0]);
378
+ if (positionals.length > 1) {
379
+ fail(`impel pat revoke: unexpected argument "${redactSecretText(positionals[1])}".`);
380
+ }
381
+ if (flags.yes !== true) {
382
+ fail("impel pat revoke: refusing to revoke without --yes.");
383
+ }
384
+ const { appUrl, config } = requireConfig(flags);
385
+ const revoked = await revokePat({ appUrl, currentPat: config.pat, tokenId });
386
+ printRevokedPat(revoked, flags.json === true);
387
+ return;
388
+ }
389
+
390
+ rejectUnsupportedFlags(flags, CREATE_FLAG_NAMES, "create");
305
391
  if (positionals.length > 0) {
306
392
  fail(`impel pat create: unexpected argument "${redactSecretText(positionals[0])}".`);
307
393
  }