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 +16 -1
- package/package.json +1 -1
- package/src/cli.js +1 -0
- package/src/commands/pat.js +99 -13
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 ...`
|
|
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
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
|
package/src/commands/pat.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
224
|
-
method
|
|
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
|
|
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
|
|
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
|
}
|