@alter-ai/cli 0.3.2 → 0.3.3
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/dist/cli.js +532 -130
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -173,7 +173,7 @@ async function maybePrintUpdateBanner(currentVersion, argv2 = process.argv.slice
|
|
|
173
173
|
// package.json
|
|
174
174
|
var package_default = {
|
|
175
175
|
name: "@alter-ai/cli",
|
|
176
|
-
version: "0.3.
|
|
176
|
+
version: "0.3.3",
|
|
177
177
|
description: "Command-line interface for the Alter Vault dev portal \u2014 scripted dashboard automation.",
|
|
178
178
|
type: "module",
|
|
179
179
|
bin: {
|
|
@@ -216,7 +216,7 @@ var package_default = {
|
|
|
216
216
|
tsup: "^8.0.0",
|
|
217
217
|
tsx: "^4.7.0",
|
|
218
218
|
typescript: "^5.5.0",
|
|
219
|
-
vitest: "^
|
|
219
|
+
vitest: "^4.1.8"
|
|
220
220
|
},
|
|
221
221
|
engines: {
|
|
222
222
|
node: ">=20.10.0"
|
|
@@ -224,7 +224,7 @@ var package_default = {
|
|
|
224
224
|
};
|
|
225
225
|
|
|
226
226
|
// src/program.ts
|
|
227
|
-
import { Command as
|
|
227
|
+
import { Command as Command15 } from "commander";
|
|
228
228
|
|
|
229
229
|
// src/commands/agents.ts
|
|
230
230
|
import { Command } from "commander";
|
|
@@ -1019,6 +1019,45 @@ var ConnectSession = class {
|
|
|
1019
1019
|
return `ConnectSession(url=${this.connectUrl}, expires_in=${this.expiresIn})`;
|
|
1020
1020
|
}
|
|
1021
1021
|
};
|
|
1022
|
+
var AuthSession = class {
|
|
1023
|
+
sessionToken;
|
|
1024
|
+
authUrl;
|
|
1025
|
+
expiresIn;
|
|
1026
|
+
expiresAt;
|
|
1027
|
+
constructor(data) {
|
|
1028
|
+
_assertString(data.session_token, "session_token", "AuthSession");
|
|
1029
|
+
_assertString(data.auth_url, "auth_url", "AuthSession");
|
|
1030
|
+
_assertString(data.expires_at, "expires_at", "AuthSession");
|
|
1031
|
+
if (typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in)) {
|
|
1032
|
+
throw new BackendError("AuthSession: 'expires_in' must be a finite number", {
|
|
1033
|
+
field: "expires_in"
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
this.sessionToken = data.session_token;
|
|
1037
|
+
this.authUrl = data.auth_url;
|
|
1038
|
+
this.expiresIn = data.expires_in;
|
|
1039
|
+
this.expiresAt = data.expires_at;
|
|
1040
|
+
Object.freeze(this);
|
|
1041
|
+
}
|
|
1042
|
+
toJSON() {
|
|
1043
|
+
return {
|
|
1044
|
+
session_token: this.sessionToken,
|
|
1045
|
+
auth_url: this.authUrl,
|
|
1046
|
+
expires_in: this.expiresIn,
|
|
1047
|
+
expires_at: this.expiresAt
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
toString() {
|
|
1051
|
+
return `AuthSession(authUrl=<redacted>, expiresIn=${this.expiresIn}, expiresAt=${JSON.stringify(this.expiresAt)})`;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Custom Node.js inspect output — redacts authUrl/sessionToken so
|
|
1055
|
+
* console.log / util.inspect can't leak them (mirrors TokenResponse).
|
|
1056
|
+
*/
|
|
1057
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
1058
|
+
return this.toString();
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1022
1061
|
var ManagedSecretConnectSession = class {
|
|
1023
1062
|
sessionToken;
|
|
1024
1063
|
connectUrl;
|
|
@@ -1318,7 +1357,8 @@ var AuthResult = class {
|
|
|
1318
1357
|
userInfo;
|
|
1319
1358
|
constructor(data) {
|
|
1320
1359
|
this.userToken = data.user_token;
|
|
1321
|
-
|
|
1360
|
+
const ui = data.user_info;
|
|
1361
|
+
this.userInfo = typeof ui === "object" && ui !== null && !Array.isArray(ui) ? ui : {};
|
|
1322
1362
|
Object.freeze(this);
|
|
1323
1363
|
}
|
|
1324
1364
|
toJSON() {
|
|
@@ -1331,6 +1371,17 @@ var AuthResult = class {
|
|
|
1331
1371
|
const sub = this.userInfo?.sub ?? "unknown";
|
|
1332
1372
|
return `AuthResult(sub=${sub})`;
|
|
1333
1373
|
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Custom Node.js inspect output — redacts the live IDP JWT `userToken` so
|
|
1376
|
+
* console.log / util.inspect can't leak it (mirrors TokenResponse and the
|
|
1377
|
+
* sibling AuthSession). Without this, util.inspect enumerates own fields and
|
|
1378
|
+
* prints the full bearer token. The Python sibling's `__repr__`/`__str__`
|
|
1379
|
+
* redacts the same field — keep parity, and uphold "NEVER expose tokens in
|
|
1380
|
+
* logs".
|
|
1381
|
+
*/
|
|
1382
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
1383
|
+
return this.toString();
|
|
1384
|
+
}
|
|
1334
1385
|
};
|
|
1335
1386
|
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
|
|
1336
1387
|
"authorization",
|
|
@@ -2940,8 +2991,11 @@ function _extractAdditionalCredentials(token) {
|
|
|
2940
2991
|
return _additionalCredsStore.get(token);
|
|
2941
2992
|
}
|
|
2942
2993
|
var _fetch;
|
|
2943
|
-
var SDK_VERSION = "0.
|
|
2994
|
+
var SDK_VERSION = "0.16.0";
|
|
2944
2995
|
var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
|
|
2996
|
+
var AUTH_POLL_SERVER_WAIT_MS = 25e3;
|
|
2997
|
+
var AUTH_POLL_HTTP_BUFFER_MS = 15e3;
|
|
2998
|
+
var PERMANENT_POLL_STATUSES = /* @__PURE__ */ new Set([400, 401, 403, 404, 422]);
|
|
2945
2999
|
var HTTP_FORBIDDEN = 403;
|
|
2946
3000
|
var HTTP_NO_CONTENT2 = 204;
|
|
2947
3001
|
var HTTP_NOT_FOUND = 404;
|
|
@@ -3068,10 +3122,11 @@ var HttpClient = class {
|
|
|
3068
3122
|
...this.#defaultHeaders,
|
|
3069
3123
|
...options?.headers
|
|
3070
3124
|
};
|
|
3125
|
+
const effectiveTimeoutMs = options?.timeoutMs ?? this.#timeoutMs;
|
|
3071
3126
|
const controller = new AbortController();
|
|
3072
3127
|
const timeoutId = setTimeout(() => {
|
|
3073
3128
|
controller.abort(new DOMException("The operation timed out.", "TimeoutError"));
|
|
3074
|
-
},
|
|
3129
|
+
}, effectiveTimeoutMs);
|
|
3075
3130
|
const init = {
|
|
3076
3131
|
method,
|
|
3077
3132
|
headers: mergedHeaders,
|
|
@@ -5194,33 +5249,31 @@ ${effectiveConstraints}`;
|
|
|
5194
5249
|
});
|
|
5195
5250
|
}
|
|
5196
5251
|
/**
|
|
5197
|
-
*
|
|
5252
|
+
* Mint an end-user IDP sign-in session (link-based, headless).
|
|
5198
5253
|
*
|
|
5199
|
-
*
|
|
5200
|
-
*
|
|
5254
|
+
* The split, pure counterpart to {@link authenticate}. Returns an
|
|
5255
|
+
* {@link AuthSession} carrying the IDP `authUrl` to hand to the end user
|
|
5256
|
+
* and a `sessionToken` to poll with {@link pollAuthSession}. Unlike
|
|
5257
|
+
* `authenticate`, this method:
|
|
5201
5258
|
*
|
|
5202
|
-
*
|
|
5203
|
-
*
|
|
5204
|
-
*
|
|
5259
|
+
* - does **not** open a browser (hand `authUrl` to a remote user over any
|
|
5260
|
+
* channel — a Slack DM, an MCP client, a printed link), and
|
|
5261
|
+
* - does **not** install a `userTokenGetter` on the instance, so it is
|
|
5262
|
+
* safe on a shared SDK instance that resolves a JWT per request.
|
|
5205
5263
|
*
|
|
5206
|
-
*
|
|
5264
|
+
* `sessionToken` is a persistable handle: a headless consumer can seal it,
|
|
5265
|
+
* poll in a background loop, and resume a pending session after a restart.
|
|
5207
5266
|
*
|
|
5208
|
-
*
|
|
5209
|
-
*
|
|
5210
|
-
* call `authenticate()`, the constructor getter is silently replaced for
|
|
5211
|
-
* the lifetime of the instance — a fresh `_VaultClient` instance is the
|
|
5212
|
-
* only way to restore the original getter. This is intentional for
|
|
5213
|
-
* interactive CLI flows but can surprise web backends that mix both
|
|
5214
|
-
* patterns. Prefer one or the other per `_VaultClient` instance.
|
|
5267
|
+
* Scope: requires an app key with the `idp_users:write` scope. Sign-in is
|
|
5268
|
+
* app-initiated (agents do not start user logins).
|
|
5215
5269
|
*
|
|
5216
|
-
* @
|
|
5217
|
-
* @
|
|
5218
|
-
* @throws
|
|
5219
|
-
* @throws
|
|
5270
|
+
* @returns AuthSession with `authUrl`, `sessionToken`, `expiresIn`, `expiresAt`.
|
|
5271
|
+
* @throws AlterSDKError if the SDK is closed or the backend rejects the request.
|
|
5272
|
+
* @throws NetworkError if the backend is unreachable.
|
|
5273
|
+
* @throws TimeoutError if the request to the backend times out.
|
|
5220
5274
|
*/
|
|
5221
|
-
async
|
|
5275
|
+
async createAuthSession() {
|
|
5222
5276
|
this.#assertNotClosed();
|
|
5223
|
-
const timeoutMs = options?.timeout ?? 3e5;
|
|
5224
5277
|
const actorHeaders = this.#getActorRequestHeaders();
|
|
5225
5278
|
const sessionPath = "/sdk/auth/session";
|
|
5226
5279
|
const sessionBodyStr = JSON.stringify({});
|
|
@@ -5250,64 +5303,199 @@ ${effectiveConstraints}`;
|
|
|
5250
5303
|
}
|
|
5251
5304
|
this.#cacheActorIdFromResponse(sessionResp);
|
|
5252
5305
|
await this.#handleErrorResponse(sessionResp);
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5306
|
+
let data;
|
|
5307
|
+
try {
|
|
5308
|
+
data = await sessionResp.json();
|
|
5309
|
+
} catch {
|
|
5310
|
+
throw new BackendError("Auth session response body is not valid JSON");
|
|
5311
|
+
}
|
|
5312
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
5313
|
+
throw new BackendError("Auth session response body is not a JSON object");
|
|
5314
|
+
}
|
|
5315
|
+
return new AuthSession(
|
|
5316
|
+
data
|
|
5257
5317
|
);
|
|
5258
|
-
|
|
5318
|
+
}
|
|
5319
|
+
/**
|
|
5320
|
+
* Poll an end-user sign-in session to completion.
|
|
5321
|
+
*
|
|
5322
|
+
* The polling half of the link-based sign-in flow. Use this when your code
|
|
5323
|
+
* minted the session itself via {@link createAuthSession} and you need to
|
|
5324
|
+
* block until the user finishes IDP login. {@link authenticate} is the
|
|
5325
|
+
* all-in-one CLI convenience (mint + open browser + poll + install token);
|
|
5326
|
+
* this method is the polling half on its own and — like
|
|
5327
|
+
* {@link createAuthSession} — installs **no** `userTokenGetter`. The caller
|
|
5328
|
+
* decides what to do with the returned token.
|
|
5329
|
+
*
|
|
5330
|
+
* Transient network blips and non-200 responses are retried until the
|
|
5331
|
+
* deadline (a remote user may take minutes to complete login); only a
|
|
5332
|
+
* terminal IDP error, an expired session, or the timeout end the loop.
|
|
5333
|
+
*
|
|
5334
|
+
* Scope: requires an app key with the `idp_users:read` scope.
|
|
5335
|
+
*
|
|
5336
|
+
* @param sessionToken - A session token from {@link createAuthSession}.
|
|
5337
|
+
* @param options - `timeoutMs` (default 300000) and `pollIntervalMs`
|
|
5338
|
+
* (default 2000). The Python SDK's equivalent takes seconds by ecosystem
|
|
5339
|
+
* convention; the two SDKs intentionally differ in unit.
|
|
5340
|
+
* @returns AuthResult with the user's IDP JWT (`userToken`) and `userInfo`.
|
|
5341
|
+
* @throws AlterValueError if `sessionToken` is empty.
|
|
5342
|
+
* @throws AlterSDKError if the SDK is closed or the IDP returned a terminal
|
|
5343
|
+
* error / the session expired.
|
|
5344
|
+
* @throws ConnectTimeoutError if the user did not complete login in time.
|
|
5345
|
+
*/
|
|
5346
|
+
async pollAuthSession(sessionToken, options) {
|
|
5347
|
+
this.#assertNotClosed();
|
|
5348
|
+
if (typeof sessionToken !== "string" || sessionToken.trim().length === 0) {
|
|
5349
|
+
throw new AlterValueError("sessionToken must be a non-empty string");
|
|
5350
|
+
}
|
|
5351
|
+
const timeoutMs = options?.timeoutMs ?? 3e5;
|
|
5352
|
+
const pollIntervalMs = options?.pollIntervalMs ?? 2e3;
|
|
5353
|
+
const deadline = Date.now() + timeoutMs;
|
|
5259
5354
|
while (true) {
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
)
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
continue;
|
|
5282
|
-
}
|
|
5283
|
-
const pollData = await pollResp.json();
|
|
5284
|
-
if (pollData.status === "completed") {
|
|
5285
|
-
const userToken = pollData.user_token;
|
|
5286
|
-
if (!userToken) {
|
|
5355
|
+
this.#assertNotClosed();
|
|
5356
|
+
const remainingBudget = deadline - Date.now();
|
|
5357
|
+
const waitMs = Math.max(
|
|
5358
|
+
0,
|
|
5359
|
+
Math.min(Math.max(remainingBudget, 0), AUTH_POLL_SERVER_WAIT_MS)
|
|
5360
|
+
);
|
|
5361
|
+
const pollData = await this.#pollAuthSessionOnce(sessionToken, waitMs);
|
|
5362
|
+
if (pollData) {
|
|
5363
|
+
if (pollData.status === "completed") {
|
|
5364
|
+
const userToken = pollData.user_token;
|
|
5365
|
+
if (!userToken) {
|
|
5366
|
+
throw new AlterSDKError(
|
|
5367
|
+
"Authentication completed but no user token was returned by the IDP"
|
|
5368
|
+
);
|
|
5369
|
+
}
|
|
5370
|
+
return new AuthResult({
|
|
5371
|
+
user_token: userToken,
|
|
5372
|
+
user_info: pollData.user_info
|
|
5373
|
+
});
|
|
5374
|
+
}
|
|
5375
|
+
if (pollData.status === "error") {
|
|
5287
5376
|
throw new AlterSDKError(
|
|
5288
|
-
|
|
5377
|
+
`Authentication failed: ${pollData.error_message || "unknown error"}`
|
|
5289
5378
|
);
|
|
5290
5379
|
}
|
|
5291
|
-
if (
|
|
5292
|
-
|
|
5293
|
-
|
|
5380
|
+
if (pollData.status === "expired") {
|
|
5381
|
+
throw new AlterSDKError("Authentication session expired");
|
|
5382
|
+
}
|
|
5383
|
+
if (pollData.status !== "pending") {
|
|
5384
|
+
throw new AlterSDKError(
|
|
5385
|
+
`Unexpected auth poll status from server: ${String(pollData.status)}`
|
|
5294
5386
|
);
|
|
5295
5387
|
}
|
|
5296
|
-
this.#userTokenGetter = () => userToken;
|
|
5297
|
-
return new AuthResult({
|
|
5298
|
-
user_token: userToken,
|
|
5299
|
-
user_info: pollData.user_info
|
|
5300
|
-
});
|
|
5301
5388
|
}
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
)
|
|
5389
|
+
const remaining = deadline - Date.now();
|
|
5390
|
+
if (remaining <= 0) break;
|
|
5391
|
+
await new Promise(
|
|
5392
|
+
(resolve2) => setTimeout(resolve2, Math.min(pollIntervalMs, remaining))
|
|
5393
|
+
);
|
|
5394
|
+
}
|
|
5395
|
+
throw new ConnectTimeoutError("Authentication timed out", { timeoutMs });
|
|
5396
|
+
}
|
|
5397
|
+
/**
|
|
5398
|
+
* Single sign-in poll attempt (INTERNAL).
|
|
5399
|
+
*
|
|
5400
|
+
* Returns the parsed poll body, or `null` to signal a transient failure
|
|
5401
|
+
* (network blip or non-200) that the caller should retry. Matches the
|
|
5402
|
+
* Python SDK's resilient poll: a long sign-in poll survives brief server
|
|
5403
|
+
* hiccups rather than aborting the whole flow.
|
|
5404
|
+
*/
|
|
5405
|
+
async #pollAuthSessionOnce(sessionToken, waitMs = 0) {
|
|
5406
|
+
const actorHeaders = this.#getActorRequestHeaders();
|
|
5407
|
+
const pollPath = "/sdk/auth/poll";
|
|
5408
|
+
const pollBody = { session_token: sessionToken };
|
|
5409
|
+
if (waitMs > 0) pollBody.wait_ms = waitMs;
|
|
5410
|
+
const pollBodyStr = JSON.stringify(pollBody);
|
|
5411
|
+
const pollHmac = this.#computeHmacHeaders("POST", pollPath, pollBodyStr);
|
|
5412
|
+
const requestTimeoutMs = waitMs > 0 ? waitMs + AUTH_POLL_HTTP_BUFFER_MS : void 0;
|
|
5413
|
+
let pollResp;
|
|
5414
|
+
try {
|
|
5415
|
+
pollResp = await this.#alterClient.post(pollPath, {
|
|
5416
|
+
body: pollBodyStr,
|
|
5417
|
+
headers: { ...actorHeaders, ...pollHmac, "Content-Type": "application/json" },
|
|
5418
|
+
timeoutMs: requestTimeoutMs
|
|
5419
|
+
});
|
|
5420
|
+
} catch (e) {
|
|
5421
|
+
if (e instanceof TypeError || e instanceof Error && (e.name === "TimeoutError" || e.name === "AbortError")) {
|
|
5422
|
+
return null;
|
|
5306
5423
|
}
|
|
5307
|
-
|
|
5308
|
-
|
|
5424
|
+
throw e;
|
|
5425
|
+
}
|
|
5426
|
+
this.#cacheActorIdFromResponse(pollResp);
|
|
5427
|
+
if (!pollResp.ok) {
|
|
5428
|
+
if (PERMANENT_POLL_STATUSES.has(pollResp.status)) {
|
|
5429
|
+
await this.#handleErrorResponse(pollResp);
|
|
5309
5430
|
}
|
|
5431
|
+
return null;
|
|
5432
|
+
}
|
|
5433
|
+
let parsed;
|
|
5434
|
+
try {
|
|
5435
|
+
parsed = await pollResp.json();
|
|
5436
|
+
} catch {
|
|
5437
|
+
throw new BackendError("Auth session poll response body is not valid JSON");
|
|
5438
|
+
}
|
|
5439
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
5440
|
+
throw new BackendError("Auth session poll response body is not a JSON object");
|
|
5441
|
+
}
|
|
5442
|
+
const data = parsed;
|
|
5443
|
+
if (typeof data.status !== "string" || data.status.length === 0) {
|
|
5444
|
+
throw new BackendError(
|
|
5445
|
+
"Auth session poll response missing required 'status' field",
|
|
5446
|
+
{ field: "status" }
|
|
5447
|
+
);
|
|
5448
|
+
}
|
|
5449
|
+
return data;
|
|
5450
|
+
}
|
|
5451
|
+
/**
|
|
5452
|
+
* Trigger IDP login for end user via browser (bundled CLI convenience).
|
|
5453
|
+
*
|
|
5454
|
+
* Opens the app's configured IDP login page in the user's default browser.
|
|
5455
|
+
* Polls for completion and returns the user's IDP JWT token.
|
|
5456
|
+
*
|
|
5457
|
+
* After `authenticate()` completes, the returned `userToken` is installed
|
|
5458
|
+
* as the active `userTokenGetter` on this instance and automatically used
|
|
5459
|
+
* for subsequent `request()` calls that use identity resolution.
|
|
5460
|
+
*
|
|
5461
|
+
* This composes the two split primitives: {@link createAuthSession} (mint) +
|
|
5462
|
+
* {@link pollAuthSession} (poll). For headless / remote / multi-tenant
|
|
5463
|
+
* sign-in, call the primitives directly — they open no browser and install
|
|
5464
|
+
* no `userTokenGetter`.
|
|
5465
|
+
*
|
|
5466
|
+
* .. warning::
|
|
5467
|
+
*
|
|
5468
|
+
* **`authenticate()` overwrites any constructor-supplied `userTokenGetter`.**
|
|
5469
|
+
* If you pass `userTokenGetter` to the `_VaultClient` constructor and then
|
|
5470
|
+
* call `authenticate()`, the constructor getter is silently replaced for
|
|
5471
|
+
* the lifetime of the instance — a fresh `_VaultClient` instance is the
|
|
5472
|
+
* only way to restore the original getter. This is intentional for
|
|
5473
|
+
* interactive CLI flows but can surprise web backends that mix both
|
|
5474
|
+
* patterns. Prefer one or the other per `_VaultClient` instance. The
|
|
5475
|
+
* split primitives never install a getter, so prefer them when this
|
|
5476
|
+
* side effect is unwanted.
|
|
5477
|
+
*
|
|
5478
|
+
* @param options - Optional configuration (timeout in milliseconds, default 300000 = 5 min)
|
|
5479
|
+
* @returns AuthResult with userToken and userInfo
|
|
5480
|
+
* @throws ConnectTimeoutError if authentication times out
|
|
5481
|
+
* @throws AlterSDKError if session creation or authentication fails
|
|
5482
|
+
*/
|
|
5483
|
+
async authenticate(options) {
|
|
5484
|
+
this.#assertNotClosed();
|
|
5485
|
+
const timeoutMs = options?.timeout ?? 3e5;
|
|
5486
|
+
const session = await this.createAuthSession();
|
|
5487
|
+
await __VaultClient.#openBrowserOrPrint(
|
|
5488
|
+
session.authUrl,
|
|
5489
|
+
"Open this URL to authenticate"
|
|
5490
|
+
);
|
|
5491
|
+
const result = await this.pollAuthSession(session.sessionToken, { timeoutMs });
|
|
5492
|
+
if (this.#userTokenGetter !== null) {
|
|
5493
|
+
this.#logger.warn(
|
|
5494
|
+
"[alter-sdk] authenticate() replaced a previously-set userTokenGetter on this _VaultClient instance. Subsequent requests will use the authenticate() token, not the constructor getter."
|
|
5495
|
+
);
|
|
5310
5496
|
}
|
|
5497
|
+
this.#userTokenGetter = () => result.userToken;
|
|
5498
|
+
return result;
|
|
5311
5499
|
}
|
|
5312
5500
|
/**
|
|
5313
5501
|
* Poll the Connect session for completion status (INTERNAL).
|
|
@@ -6439,6 +6627,20 @@ var App = class _App {
|
|
|
6439
6627
|
return this.#client.createConnectSessionForError(...args);
|
|
6440
6628
|
}
|
|
6441
6629
|
// ── Auth (operator-only) ───────────────────────────────────────────────
|
|
6630
|
+
/**
|
|
6631
|
+
* Mint a link-based end-user sign-in session (no browser, no instance
|
|
6632
|
+
* mutation). See {@link _VaultClient.createAuthSession}.
|
|
6633
|
+
*/
|
|
6634
|
+
async createAuthSession() {
|
|
6635
|
+
return this.#client.createAuthSession();
|
|
6636
|
+
}
|
|
6637
|
+
/**
|
|
6638
|
+
* Poll a sign-in session to completion, returning the user's JWT.
|
|
6639
|
+
* See {@link _VaultClient.pollAuthSession}.
|
|
6640
|
+
*/
|
|
6641
|
+
async pollAuthSession(...args) {
|
|
6642
|
+
return this.#client.pollAuthSession(...args);
|
|
6643
|
+
}
|
|
6442
6644
|
async authenticate(options) {
|
|
6443
6645
|
return this.#client.authenticate(options);
|
|
6444
6646
|
}
|
|
@@ -6871,7 +7073,7 @@ var DEFAULT_BASE_URL = "https://backend.alterauth.com";
|
|
|
6871
7073
|
var PAT_API_PREFIX = "/api/v1/dev-portal";
|
|
6872
7074
|
var HTTP_ERROR_THRESHOLD = 400;
|
|
6873
7075
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
6874
|
-
var CLI_VERSION = "0.3.
|
|
7076
|
+
var CLI_VERSION = "0.3.3";
|
|
6875
7077
|
var USER_AGENT = buildUserAgent();
|
|
6876
7078
|
function buildUserAgent() {
|
|
6877
7079
|
let osTag = "";
|
|
@@ -7017,6 +7219,9 @@ var DashboardClient = class {
|
|
|
7017
7219
|
policy;
|
|
7018
7220
|
/** Audit log read surface (CLI `alter audit` subcommands). */
|
|
7019
7221
|
audit;
|
|
7222
|
+
/** OAuth + managed-secret grant inspect/revoke (CLI `alter grants`
|
|
7223
|
+
* subcommands). Requires ``dashboard_grants:read`` / ``:admin``. */
|
|
7224
|
+
grants;
|
|
7020
7225
|
#closed = false;
|
|
7021
7226
|
constructor(options = {}) {
|
|
7022
7227
|
const resolvedPat = options.pat ?? (typeof process !== "undefined" ? process.env.ALTER_PAT : void 0);
|
|
@@ -7066,6 +7271,7 @@ var DashboardClient = class {
|
|
|
7066
7271
|
this.managedSecrets = new ManagedSecretsNamespace(this);
|
|
7067
7272
|
this.policy = new PolicyNamespace(this);
|
|
7068
7273
|
this.audit = new AuditNamespace(this);
|
|
7274
|
+
this.grants = new GrantsNamespace(this);
|
|
7069
7275
|
}
|
|
7070
7276
|
/**
|
|
7071
7277
|
* Release internal state. The default `fetch` is a global; this is
|
|
@@ -8029,9 +8235,8 @@ var AuditNamespace = class {
|
|
|
8029
8235
|
}
|
|
8030
8236
|
/** List runtime audit-log rows. Requires `dashboard_audit:read`.
|
|
8031
8237
|
*
|
|
8032
|
-
*
|
|
8033
|
-
*
|
|
8034
|
-
* ``items`` shape). Consumers narrow ``logs`` themselves.
|
|
8238
|
+
* Returns the canonical envelope ``{items, total, limit, offset, has_more}``
|
|
8239
|
+
* plus the distinct ``is_truncated`` cap-signal (the 10k offset cap was hit).
|
|
8035
8240
|
*/
|
|
8036
8241
|
async list(options = {}) {
|
|
8037
8242
|
const query = optionsToQuery({
|
|
@@ -8044,8 +8249,8 @@ var AuditNamespace = class {
|
|
|
8044
8249
|
}
|
|
8045
8250
|
/** List dashboard / CLI admin actions. Requires `dashboard_audit:read`.
|
|
8046
8251
|
*
|
|
8047
|
-
*
|
|
8048
|
-
* ``
|
|
8252
|
+
* Returns the canonical envelope ``{items, total, limit, offset, has_more}``
|
|
8253
|
+
* plus the distinct ``is_truncated`` cap-signal.
|
|
8049
8254
|
*/
|
|
8050
8255
|
async listPortalActions(options = {}) {
|
|
8051
8256
|
const query = optionsToQuery({
|
|
@@ -8063,8 +8268,8 @@ var AuditNamespace = class {
|
|
|
8063
8268
|
}
|
|
8064
8269
|
/** List grant-lifecycle events. Requires `dashboard_audit:read`.
|
|
8065
8270
|
*
|
|
8066
|
-
*
|
|
8067
|
-
* ``
|
|
8271
|
+
* Returns the canonical envelope ``{items, total, limit, offset, has_more}``
|
|
8272
|
+
* plus the distinct ``is_truncated`` cap-signal.
|
|
8068
8273
|
*/
|
|
8069
8274
|
async listGrantEvents(options = {}) {
|
|
8070
8275
|
const query = optionsToQuery({
|
|
@@ -8082,8 +8287,8 @@ var AuditNamespace = class {
|
|
|
8082
8287
|
}
|
|
8083
8288
|
/** List trace summaries. Requires `dashboard_audit:read`.
|
|
8084
8289
|
*
|
|
8085
|
-
*
|
|
8086
|
-
* ``
|
|
8290
|
+
* Returns the canonical envelope ``{items, total, limit, offset, has_more}``
|
|
8291
|
+
* plus the distinct ``is_event_window_capped`` signal.
|
|
8087
8292
|
*/
|
|
8088
8293
|
async listTraces(options = {}) {
|
|
8089
8294
|
const query = optionsToQuery({
|
|
@@ -8110,6 +8315,59 @@ var AuditNamespace = class {
|
|
|
8110
8315
|
return expectDict(body, "audit.get_trace", 200);
|
|
8111
8316
|
}
|
|
8112
8317
|
};
|
|
8318
|
+
var GrantsNamespace = class {
|
|
8319
|
+
#client;
|
|
8320
|
+
constructor(client) {
|
|
8321
|
+
this.#client = client;
|
|
8322
|
+
}
|
|
8323
|
+
/**
|
|
8324
|
+
* List grants on an app. Returns the polymorphic OAuth +
|
|
8325
|
+
* managed-secret list (each row carries a ``grant_type``
|
|
8326
|
+
* discriminator). Requires ``dashboard_grants:read``.
|
|
8327
|
+
*
|
|
8328
|
+
* Wire envelope: ``{ grants, total, limit, offset, has_more,
|
|
8329
|
+
* is_truncated }`` — non-canonical (pre-dates the ``items``
|
|
8330
|
+
* convention). Consumers narrow ``grants`` themselves.
|
|
8331
|
+
*/
|
|
8332
|
+
async list(appId, options = {}) {
|
|
8333
|
+
const app = encodePathParam(appId, "appId");
|
|
8334
|
+
const query = optionsToQuery({
|
|
8335
|
+
// The backend's FastAPI Query param on this GET route is named
|
|
8336
|
+
// ``grant_status`` (no alias). The public-facing CLI flag is
|
|
8337
|
+
// ``--status`` for ergonomics; we map it to the wire name here.
|
|
8338
|
+
grant_status: options.status,
|
|
8339
|
+
provider_id: options.provider_id,
|
|
8340
|
+
app_user_id: options.app_user_id,
|
|
8341
|
+
search: options.search,
|
|
8342
|
+
limit: options.limit ?? 100,
|
|
8343
|
+
offset: options.offset ?? 0
|
|
8344
|
+
});
|
|
8345
|
+
const body = await this.#client._call(
|
|
8346
|
+
"GET",
|
|
8347
|
+
`/apps/${app}/grants`,
|
|
8348
|
+
"grants.list",
|
|
8349
|
+
{ query }
|
|
8350
|
+
);
|
|
8351
|
+
return expectDict(body, "grants.list", 200);
|
|
8352
|
+
}
|
|
8353
|
+
/**
|
|
8354
|
+
* Revoke a grant on an app. Cascade-revokes any active agent
|
|
8355
|
+
* delegations under the grant and best-effort deletes the vault
|
|
8356
|
+
* token. Recoverable via re-consent through the OAuth flow.
|
|
8357
|
+
* Requires ``dashboard_grants:admin``.
|
|
8358
|
+
*/
|
|
8359
|
+
async revoke(appId, grantId, options = {}) {
|
|
8360
|
+
const app = encodePathParam(appId, "appId");
|
|
8361
|
+
const grant = encodePathParam(grantId, "grantId");
|
|
8362
|
+
const body = await this.#client._call(
|
|
8363
|
+
"POST",
|
|
8364
|
+
`/apps/${app}/grants/${grant}/revoke`,
|
|
8365
|
+
"grants.revoke",
|
|
8366
|
+
{ jsonBody: { reason: options.reason ?? null } }
|
|
8367
|
+
);
|
|
8368
|
+
return expectDict(body, "grants.revoke", 200);
|
|
8369
|
+
}
|
|
8370
|
+
};
|
|
8113
8371
|
|
|
8114
8372
|
// src/portal-factory.ts
|
|
8115
8373
|
async function createPortalClient() {
|
|
@@ -9299,10 +9557,6 @@ function parseRelativeFromNow(raw, direction) {
|
|
|
9299
9557
|
}
|
|
9300
9558
|
|
|
9301
9559
|
// src/commands/audit.ts
|
|
9302
|
-
function normalizeAuditEnvelope(result, domainKey) {
|
|
9303
|
-
const { [domainKey]: items, ...rest } = result;
|
|
9304
|
-
return { ...rest, items };
|
|
9305
|
-
}
|
|
9306
9560
|
function resolveStartDate(start, since) {
|
|
9307
9561
|
if (start !== void 0 && since !== void 0) {
|
|
9308
9562
|
process.stderr.write(
|
|
@@ -9415,7 +9669,7 @@ function buildAuditCommand() {
|
|
|
9415
9669
|
offset: options.offset
|
|
9416
9670
|
});
|
|
9417
9671
|
const logs = validateRows(
|
|
9418
|
-
extractArrayField(result, "
|
|
9672
|
+
extractArrayField(result, "items", "audit.list"),
|
|
9419
9673
|
{
|
|
9420
9674
|
id: isString,
|
|
9421
9675
|
timestamp: isString,
|
|
@@ -9434,7 +9688,7 @@ function buildAuditCommand() {
|
|
|
9434
9688
|
if (format === "table" || format === "jsonl") {
|
|
9435
9689
|
emit(format, logs, AUDIT_COLUMNS);
|
|
9436
9690
|
} else {
|
|
9437
|
-
emit(format,
|
|
9691
|
+
emit(format, result);
|
|
9438
9692
|
}
|
|
9439
9693
|
});
|
|
9440
9694
|
}
|
|
@@ -9481,7 +9735,7 @@ function buildAuditCommand() {
|
|
|
9481
9735
|
offset: options.offset
|
|
9482
9736
|
});
|
|
9483
9737
|
const events = validateRows(
|
|
9484
|
-
extractArrayField(result, "
|
|
9738
|
+
extractArrayField(result, "items", "audit.list_portal_actions"),
|
|
9485
9739
|
{
|
|
9486
9740
|
id: isString,
|
|
9487
9741
|
timestamp: isString,
|
|
@@ -9496,7 +9750,7 @@ function buildAuditCommand() {
|
|
|
9496
9750
|
if (format === "table" || format === "jsonl") {
|
|
9497
9751
|
emit(format, events, PORTAL_ACTION_COLUMNS);
|
|
9498
9752
|
} else {
|
|
9499
|
-
emit(format,
|
|
9753
|
+
emit(format, result);
|
|
9500
9754
|
}
|
|
9501
9755
|
});
|
|
9502
9756
|
}
|
|
@@ -9532,7 +9786,7 @@ function buildAuditCommand() {
|
|
|
9532
9786
|
offset: options.offset
|
|
9533
9787
|
});
|
|
9534
9788
|
const events = validateRows(
|
|
9535
|
-
extractArrayField(result, "
|
|
9789
|
+
extractArrayField(result, "items", "audit.list_grant_events"),
|
|
9536
9790
|
{
|
|
9537
9791
|
id: isString,
|
|
9538
9792
|
timestamp: isString,
|
|
@@ -9546,7 +9800,7 @@ function buildAuditCommand() {
|
|
|
9546
9800
|
if (format === "table" || format === "jsonl") {
|
|
9547
9801
|
emit(format, events, GRANT_EVENT_COLUMNS);
|
|
9548
9802
|
} else {
|
|
9549
|
-
emit(format,
|
|
9803
|
+
emit(format, result);
|
|
9550
9804
|
}
|
|
9551
9805
|
});
|
|
9552
9806
|
}
|
|
@@ -9573,7 +9827,7 @@ function buildAuditCommand() {
|
|
|
9573
9827
|
offset: options.offset
|
|
9574
9828
|
});
|
|
9575
9829
|
const traces = validateRows(
|
|
9576
|
-
extractArrayField(result, "
|
|
9830
|
+
extractArrayField(result, "items", "audit.list_traces"),
|
|
9577
9831
|
{
|
|
9578
9832
|
trace_id: isString,
|
|
9579
9833
|
first_timestamp: isString,
|
|
@@ -9588,7 +9842,7 @@ function buildAuditCommand() {
|
|
|
9588
9842
|
if (format === "table" || format === "jsonl") {
|
|
9589
9843
|
emit(format, traces, TRACE_COLUMNS);
|
|
9590
9844
|
} else {
|
|
9591
|
-
emit(format,
|
|
9845
|
+
emit(format, result);
|
|
9592
9846
|
}
|
|
9593
9847
|
});
|
|
9594
9848
|
}
|
|
@@ -9630,7 +9884,13 @@ var DEFAULT_SCOPES = [
|
|
|
9630
9884
|
// ``:delete`` and ``:admin`` are deliberate opt-ins via ``--scopes``
|
|
9631
9885
|
// per the destructive-action policy in CLAUDE.md.
|
|
9632
9886
|
"dashboard_secrets:read",
|
|
9633
|
-
"dashboard_secrets:write"
|
|
9887
|
+
"dashboard_secrets:write",
|
|
9888
|
+
// ``dashboard_grants`` (added to backend catalog v4). Read is in
|
|
9889
|
+
// defaults so ``alter grants list`` works out of the box; ``:admin``
|
|
9890
|
+
// (revoke) requires explicit ``--scopes`` opt-in per the
|
|
9891
|
+
// destructive-action policy in CLAUDE.md — same posture as
|
|
9892
|
+
// ``dashboard_keys:admin``.
|
|
9893
|
+
"dashboard_grants:read"
|
|
9634
9894
|
];
|
|
9635
9895
|
function deriveDashboardUrl(baseUrl) {
|
|
9636
9896
|
return baseUrl.replace(/^https:\/\/backend\./, "https://portal.");
|
|
@@ -9920,7 +10180,17 @@ var DASHBOARD_RESOURCE_VERBS = {
|
|
|
9920
10180
|
// grant + delegation + audit anchor and vault-deletes the stored
|
|
9921
10181
|
// credential (irrecoverable). Backend catalog v3, see
|
|
9922
10182
|
// ``apps/backend/app/core/scopes/catalog.py`` _DASHBOARD_RESOURCES_V3.
|
|
9923
|
-
dashboard_secrets: ["read", "write", "delete"]
|
|
10183
|
+
dashboard_secrets: ["read", "write", "delete"],
|
|
10184
|
+
// ``dashboard_grants`` (catalog v4): inspect + revoke end-user
|
|
10185
|
+
// grants on an app. ``:read`` → GET /apps/{id}/grants (list, OAuth
|
|
10186
|
+
// + managed-secret polymorphic); ``:admin`` → POST /apps/{id}/grants/
|
|
10187
|
+
// {id}/revoke. Grant revocation is RECOVERABLE per CLAUDE.md's
|
|
10188
|
+
// destructive-action tiers (re-consent restores access), so the
|
|
10189
|
+
// revoke route stays on the standard ``:admin`` verb — no separate
|
|
10190
|
+
// ``:delete`` tier. ``:write`` omitted because grants are minted by
|
|
10191
|
+
// the OAuth flow, never via direct API edits. Backend source of
|
|
10192
|
+
// truth: ``_DASHBOARD_RESOURCES_V4``.
|
|
10193
|
+
dashboard_grants: ["read", "admin"]
|
|
9924
10194
|
};
|
|
9925
10195
|
function parseScopesFlag(raw) {
|
|
9926
10196
|
if (typeof raw !== "string") {
|
|
@@ -10568,8 +10838,148 @@ function buildCompletionCommand() {
|
|
|
10568
10838
|
return completion;
|
|
10569
10839
|
}
|
|
10570
10840
|
|
|
10571
|
-
// src/commands/
|
|
10841
|
+
// src/commands/grants.ts
|
|
10572
10842
|
import { Command as Command6 } from "commander";
|
|
10843
|
+
var GRANT_COLUMNS = [
|
|
10844
|
+
{ label: "ID", get: (g) => g.id, maxWidth: 36 },
|
|
10845
|
+
{ label: "TYPE", get: (g) => g.grant_type },
|
|
10846
|
+
{ label: "PROVIDER", get: (g) => g.provider_name, maxWidth: 24 },
|
|
10847
|
+
{ label: "STATUS", get: (g) => g.status },
|
|
10848
|
+
{
|
|
10849
|
+
label: "USER",
|
|
10850
|
+
get: (g) => g.user_identifier ?? g.account_display_name ?? "\u2014",
|
|
10851
|
+
maxWidth: 32
|
|
10852
|
+
},
|
|
10853
|
+
{ label: "CREATED", get: (g) => g.created_at }
|
|
10854
|
+
];
|
|
10855
|
+
function buildGrantsCommand() {
|
|
10856
|
+
const grants = new Command6("grants").description(
|
|
10857
|
+
"Inspect and revoke end-user grants (OAuth + managed-secret)"
|
|
10858
|
+
);
|
|
10859
|
+
grants.command("list").description(
|
|
10860
|
+
"List grants on an app (OAuth + managed-secret, polymorphic). Requires dashboard_grants:read scope."
|
|
10861
|
+
).option(
|
|
10862
|
+
"--app <app-id>",
|
|
10863
|
+
"App ID. Falls back to ALTER_APP_ID env or .alter/config.yaml"
|
|
10864
|
+
).option(
|
|
10865
|
+
"--status <status>",
|
|
10866
|
+
"Filter by status: active | expired | revoked. Case-sensitive \u2014 matches the backend's ``GrantStatus`` enum verbatim."
|
|
10867
|
+
).option(
|
|
10868
|
+
"--provider <id>",
|
|
10869
|
+
"Filter by provider ID (e.g. 'google', 'github', or a managed-secret template slug)."
|
|
10870
|
+
).option(
|
|
10871
|
+
"--app-user <uuid>",
|
|
10872
|
+
"Filter by end-user (app_user_id, internal UUID)",
|
|
10873
|
+
parseUuidArgument("--app-user")
|
|
10874
|
+
).option(
|
|
10875
|
+
"--search <text>",
|
|
10876
|
+
"Substring search on user_identifier OR account_display_name (max 100 chars; backend-side filter)"
|
|
10877
|
+
).option(
|
|
10878
|
+
"--limit <n>",
|
|
10879
|
+
"Page size (1\u20131000)",
|
|
10880
|
+
parseBoundedInt("--limit", 1, 1e3),
|
|
10881
|
+
100
|
|
10882
|
+
).option(
|
|
10883
|
+
"--offset <n>",
|
|
10884
|
+
"Pagination offset (oversize values clamp to 10,000 with is_truncated=true)",
|
|
10885
|
+
parseNonNegativeInt("--offset"),
|
|
10886
|
+
0
|
|
10887
|
+
).option(
|
|
10888
|
+
"--output <format>",
|
|
10889
|
+
"Output format: json|jsonl|table (default: table)",
|
|
10890
|
+
"table"
|
|
10891
|
+
).action(
|
|
10892
|
+
async (options) => {
|
|
10893
|
+
const format = coerceOutputFormat(options.output);
|
|
10894
|
+
const status = validateChoice(
|
|
10895
|
+
"--status",
|
|
10896
|
+
options.status,
|
|
10897
|
+
["active", "expired", "revoked"]
|
|
10898
|
+
);
|
|
10899
|
+
if (options.search !== void 0) {
|
|
10900
|
+
validateNoControlCharsOrExit("--search", options.search);
|
|
10901
|
+
}
|
|
10902
|
+
await withClient(async (client) => {
|
|
10903
|
+
const result = await client.grants.list(resolveAppIdOrExit(options.app), {
|
|
10904
|
+
status,
|
|
10905
|
+
provider_id: options.provider,
|
|
10906
|
+
app_user_id: options.appUser,
|
|
10907
|
+
search: options.search,
|
|
10908
|
+
limit: options.limit,
|
|
10909
|
+
offset: options.offset
|
|
10910
|
+
});
|
|
10911
|
+
const rows = validateRows(
|
|
10912
|
+
extractArrayField(result, "grants", "grants.list"),
|
|
10913
|
+
{
|
|
10914
|
+
id: isString,
|
|
10915
|
+
grant_type: isString,
|
|
10916
|
+
provider_id: isString,
|
|
10917
|
+
provider_name: isString,
|
|
10918
|
+
status: isString,
|
|
10919
|
+
user_identifier: isOptionalString,
|
|
10920
|
+
account_display_name: isOptionalString,
|
|
10921
|
+
created_at: isString,
|
|
10922
|
+
last_used_at: isOptionalString,
|
|
10923
|
+
expires_at: isOptionalString
|
|
10924
|
+
},
|
|
10925
|
+
"grants.list"
|
|
10926
|
+
);
|
|
10927
|
+
if (format === "table" || format === "jsonl") {
|
|
10928
|
+
emit(format, rows, GRANT_COLUMNS);
|
|
10929
|
+
} else {
|
|
10930
|
+
emit(format, result);
|
|
10931
|
+
}
|
|
10932
|
+
});
|
|
10933
|
+
}
|
|
10934
|
+
);
|
|
10935
|
+
grants.command("revoke").description(
|
|
10936
|
+
"Revoke a grant immediately (cascades to active agent delegations under the grant + best-effort vault token delete). Recoverable via re-consent through the OAuth flow. Requires dashboard_grants:admin scope."
|
|
10937
|
+
).option(
|
|
10938
|
+
"--app <app-id>",
|
|
10939
|
+
"App ID. Falls back to ALTER_APP_ID env or .alter/config.yaml"
|
|
10940
|
+
).requiredOption(
|
|
10941
|
+
"--grant <grant-id>",
|
|
10942
|
+
"Grant ID",
|
|
10943
|
+
parseUuidArgument("--grant")
|
|
10944
|
+
).option(
|
|
10945
|
+
"--reason <text>",
|
|
10946
|
+
"Optional operator-supplied reason recorded on the audit row."
|
|
10947
|
+
).option("--yes", "Skip the interactive y/N prompt").action(
|
|
10948
|
+
async (options) => {
|
|
10949
|
+
if (options.reason !== void 0) {
|
|
10950
|
+
validateNoControlCharsOrExit("--reason", options.reason);
|
|
10951
|
+
}
|
|
10952
|
+
await withClient(async (client) => {
|
|
10953
|
+
if (!options.yes) {
|
|
10954
|
+
const ok = await promptYesNo(
|
|
10955
|
+
`Revoke grant ${options.grant}? This cascades to any active agent delegations. [y/N]: `
|
|
10956
|
+
);
|
|
10957
|
+
if (!ok) {
|
|
10958
|
+
process.stderr.write("alter: revoke cancelled.\n");
|
|
10959
|
+
process.exit(EXIT_CANCELLED);
|
|
10960
|
+
}
|
|
10961
|
+
}
|
|
10962
|
+
const result = await client.grants.revoke(
|
|
10963
|
+
resolveAppIdOrExit(options.app),
|
|
10964
|
+
options.grant,
|
|
10965
|
+
{ reason: options.reason }
|
|
10966
|
+
);
|
|
10967
|
+
const revokedAt = result.revoked_at;
|
|
10968
|
+
if (typeof revokedAt === "string") {
|
|
10969
|
+
process.stdout.write(`Revoked grant ${options.grant} at ${revokedAt}.
|
|
10970
|
+
`);
|
|
10971
|
+
} else {
|
|
10972
|
+
process.stdout.write(`Revoked grant ${options.grant}.
|
|
10973
|
+
`);
|
|
10974
|
+
}
|
|
10975
|
+
});
|
|
10976
|
+
}
|
|
10977
|
+
);
|
|
10978
|
+
return grants;
|
|
10979
|
+
}
|
|
10980
|
+
|
|
10981
|
+
// src/commands/keys.ts
|
|
10982
|
+
import { Command as Command7 } from "commander";
|
|
10573
10983
|
var KEY_TYPE_MAP = {
|
|
10574
10984
|
runtime: "rk",
|
|
10575
10985
|
agent: "ak"
|
|
@@ -10583,7 +10993,7 @@ var KEY_COLUMNS2 = [
|
|
|
10583
10993
|
{ label: "STATUS", get: (k) => k.revoked_at ? "revoked" : "active" }
|
|
10584
10994
|
];
|
|
10585
10995
|
function buildKeysCommand() {
|
|
10586
|
-
const keys = new
|
|
10996
|
+
const keys = new Command7("keys").description("Manage scoped API keys");
|
|
10587
10997
|
keys.command("list").description("List keys for an app").option("--app <app-id>", "App ID. Falls back to ALTER_APP_ID env or .alter/config.yaml").option(
|
|
10588
10998
|
"--output <format>",
|
|
10589
10999
|
"Output format: json|jsonl|table (default: table)",
|
|
@@ -10805,9 +11215,9 @@ function buildKeysCommand() {
|
|
|
10805
11215
|
// src/commands/link.ts
|
|
10806
11216
|
import { existsSync as existsSync4 } from "fs";
|
|
10807
11217
|
import { join as join5 } from "path";
|
|
10808
|
-
import { Command as
|
|
11218
|
+
import { Command as Command8 } from "commander";
|
|
10809
11219
|
function buildLinkCommand() {
|
|
10810
|
-
const link = new
|
|
11220
|
+
const link = new Command8("link").description(
|
|
10811
11221
|
"Pin the current directory tree to a default app_id so subsequent commands (``keys``, ``agents``, ``providers``, ``policy``) don't need --app."
|
|
10812
11222
|
).argument("[app-id]", "App ID to pin. Omitted when --status is passed.").option(
|
|
10813
11223
|
"--force",
|
|
@@ -10835,7 +11245,7 @@ function buildLinkCommand() {
|
|
|
10835
11245
|
return link;
|
|
10836
11246
|
}
|
|
10837
11247
|
function buildUnlinkCommand() {
|
|
10838
|
-
return new
|
|
11248
|
+
return new Command8("unlink").description("Remove the current directory's .alter/config.yaml pin.").action(async () => {
|
|
10839
11249
|
const removed = removeWorkspaceConfig();
|
|
10840
11250
|
if (removed) {
|
|
10841
11251
|
process.stdout.write(
|
|
@@ -10923,7 +11333,7 @@ async function runStatus() {
|
|
|
10923
11333
|
|
|
10924
11334
|
// src/commands/managed-secrets.ts
|
|
10925
11335
|
import { readFileSync as readFileSync6 } from "fs";
|
|
10926
|
-
import { Command as
|
|
11336
|
+
import { Command as Command9, Option } from "commander";
|
|
10927
11337
|
var PRINCIPAL_TYPES = ["user", "group", "system", "agent"];
|
|
10928
11338
|
var CREDENTIAL_TYPES = [
|
|
10929
11339
|
"bearer_token",
|
|
@@ -11277,7 +11687,7 @@ var SECRET_COLUMNS = [
|
|
|
11277
11687
|
{ label: "GRANTS", get: (s) => String(s.grant_count) },
|
|
11278
11688
|
{ label: "CREATED", get: (s) => s.created_at }
|
|
11279
11689
|
];
|
|
11280
|
-
var
|
|
11690
|
+
var GRANT_COLUMNS2 = [
|
|
11281
11691
|
{ label: "GRANT_ID", get: (g) => g.grant_id, maxWidth: 36 },
|
|
11282
11692
|
{ label: "PRINCIPAL", get: (g) => g.principal_type },
|
|
11283
11693
|
{ label: "LABEL", get: (g) => g.label ?? "\u2014", maxWidth: 24 },
|
|
@@ -11309,7 +11719,7 @@ var ACCESS_COLUMNS = [
|
|
|
11309
11719
|
{ label: "AGENT", get: (a) => a.via_agent_name ?? "\u2014" }
|
|
11310
11720
|
];
|
|
11311
11721
|
function buildGrantsSubcommand() {
|
|
11312
|
-
const grants = new
|
|
11722
|
+
const grants = new Command9("grants").description(
|
|
11313
11723
|
"Manage grants on managed secrets (list / create / update / revoke)."
|
|
11314
11724
|
);
|
|
11315
11725
|
grants.command("list <secret-id>").description("List grants for a managed secret").option("--app <app-id>", "App ID. Falls back to ALTER_APP_ID env or .alter/config.yaml").option(
|
|
@@ -11347,7 +11757,7 @@ function buildGrantsSubcommand() {
|
|
|
11347
11757
|
created_at: isString,
|
|
11348
11758
|
expires_at: isOptionalString
|
|
11349
11759
|
}, "managed-secrets.grants.list");
|
|
11350
|
-
emit(format, rows,
|
|
11760
|
+
emit(format, rows, GRANT_COLUMNS2);
|
|
11351
11761
|
});
|
|
11352
11762
|
}
|
|
11353
11763
|
);
|
|
@@ -11377,7 +11787,7 @@ function buildGrantsSubcommand() {
|
|
|
11377
11787
|
created_at: isString,
|
|
11378
11788
|
expires_at: isOptionalString
|
|
11379
11789
|
}, "managed-secrets.grants.list-for-agent");
|
|
11380
|
-
emit(format, rows,
|
|
11790
|
+
emit(format, rows, GRANT_COLUMNS2);
|
|
11381
11791
|
});
|
|
11382
11792
|
}
|
|
11383
11793
|
);
|
|
@@ -11581,7 +11991,7 @@ function buildGrantsSubcommand() {
|
|
|
11581
11991
|
return grants;
|
|
11582
11992
|
}
|
|
11583
11993
|
function buildGroupsSubcommand() {
|
|
11584
|
-
const groups = new
|
|
11994
|
+
const groups = new Command9("groups").description(
|
|
11585
11995
|
"App-user-group autocomplete + group detail helpers."
|
|
11586
11996
|
);
|
|
11587
11997
|
groups.command("list").description("List app-user-groups (Group-tab picker source)").option("--app <app-id>", "App ID").option("--idp <uuid>", "Filter by identity provider id").option("--search <substring>", "Substring search on group name / external_group_id").option("--limit <n>", "Page size (default: 50)", parseBoundedInt("--limit", 1, 200)).option("--offset <n>", "Starting offset (default: 0)", parseNonNegativeInt("--offset")).option(
|
|
@@ -11653,7 +12063,7 @@ function buildGroupsSubcommand() {
|
|
|
11653
12063
|
return groups;
|
|
11654
12064
|
}
|
|
11655
12065
|
function buildManagedSecretsCommand() {
|
|
11656
|
-
const root = new
|
|
12066
|
+
const root = new Command9("managed-secrets").description(
|
|
11657
12067
|
"Manage managed-secret credentials, grants, and access (CRUD + autocomplete helpers)."
|
|
11658
12068
|
);
|
|
11659
12069
|
root.command("templates").description("List the managed-secret template catalog for an app").option("--app <app-id>", "App ID").option("--limit <n>", "Page size (default: 200)", parseBoundedInt("--limit", 1, 1e3)).option("--offset <n>", "Starting offset (default: 0)", parseNonNegativeInt("--offset")).option(
|
|
@@ -11724,7 +12134,7 @@ function buildManagedSecretsCommand() {
|
|
|
11724
12134
|
root.command("create").description("Create a managed secret + base grant").option("--app <app-id>", "App ID").option(
|
|
11725
12135
|
"--input <path>",
|
|
11726
12136
|
"JSON wire body from file (@path) or stdin (-). Mirrors `gh api --input`. Replaces per-field flags; the body is sent to POST /apps/{id}/managed-secrets verbatim. Use when you have the body pre-computed (CI / templating) or want one stable shape across the CLI."
|
|
11727
|
-
).option("--
|
|
12137
|
+
).option("--name <name>", "Human display name. Required unless --input is supplied.").option("--description <text>", "Free-form description").option("--template <id>", "Template id (e.g. 'openai'); omit for custom").addOption(
|
|
11728
12138
|
// ``--credential-type`` is enum-validated at RUNTIME (see the
|
|
11729
12139
|
// per-field branch below) rather than parse-time so a typo'd
|
|
11730
12140
|
// value alongside ``--input @body.json`` (which discards the
|
|
@@ -11769,7 +12179,6 @@ function buildManagedSecretsCommand() {
|
|
|
11769
12179
|
"--input"
|
|
11770
12180
|
);
|
|
11771
12181
|
const perField = [
|
|
11772
|
-
"slug",
|
|
11773
12182
|
"name",
|
|
11774
12183
|
"description",
|
|
11775
12184
|
"template",
|
|
@@ -11802,12 +12211,6 @@ function buildManagedSecretsCommand() {
|
|
|
11802
12211
|
});
|
|
11803
12212
|
return;
|
|
11804
12213
|
}
|
|
11805
|
-
if (options.slug === void 0) {
|
|
11806
|
-
process.stderr.write(
|
|
11807
|
-
"alter: managed-secrets create requires --slug (or use --input @file.json to supply the full body)\n"
|
|
11808
|
-
);
|
|
11809
|
-
process.exit(EXIT_USAGE);
|
|
11810
|
-
}
|
|
11811
12214
|
if (options.name === void 0) {
|
|
11812
12215
|
process.stderr.write(
|
|
11813
12216
|
"alter: managed-secrets create requires --name (or use --input @file.json to supply the full body)\n"
|
|
@@ -11820,7 +12223,6 @@ function buildManagedSecretsCommand() {
|
|
|
11820
12223
|
CREDENTIAL_TYPES
|
|
11821
12224
|
);
|
|
11822
12225
|
validateNoControlCharsOrExit("--name", options.name);
|
|
11823
|
-
validateNoControlCharsOrExit("--slug", options.slug);
|
|
11824
12226
|
if (options.description !== void 0) {
|
|
11825
12227
|
validateNoControlCharsOrExit("--description", options.description);
|
|
11826
12228
|
}
|
|
@@ -11844,7 +12246,6 @@ function buildManagedSecretsCommand() {
|
|
|
11844
12246
|
const additionalCredentials = loadedCredentials !== void 0 ? loadedCredentials.additionalCredentials : options.credentialField;
|
|
11845
12247
|
const additionalInjections = options.injectionRule !== void 0 ? loadInjectionRules(options.injectionRule) : void 0;
|
|
11846
12248
|
const body = {
|
|
11847
|
-
slug: options.slug,
|
|
11848
12249
|
name: options.name,
|
|
11849
12250
|
credential_type: options.credentialType,
|
|
11850
12251
|
credential_value: credentialValue
|
|
@@ -12103,7 +12504,7 @@ function buildManagedSecretsCommand() {
|
|
|
12103
12504
|
}
|
|
12104
12505
|
|
|
12105
12506
|
// src/commands/pats.ts
|
|
12106
|
-
import { Command as
|
|
12507
|
+
import { Command as Command10 } from "commander";
|
|
12107
12508
|
async function whoami(options) {
|
|
12108
12509
|
const client = await createPortalClient();
|
|
12109
12510
|
let exitCode = 0;
|
|
@@ -12125,7 +12526,7 @@ async function whoami(options) {
|
|
|
12125
12526
|
}
|
|
12126
12527
|
}
|
|
12127
12528
|
function buildPatsCommand() {
|
|
12128
|
-
const pats = new
|
|
12529
|
+
const pats = new Command10("pats").description("Inspect Personal Access Tokens");
|
|
12129
12530
|
pats.command("whoami").description("Show the currently-active PAT and its scopes").option("--output <format>", "Output format: json|table (default: json)", "json").action(async (options) => {
|
|
12130
12531
|
const format = coerceOutputFormat(options.output);
|
|
12131
12532
|
await whoami({ output: format });
|
|
@@ -12134,9 +12535,9 @@ function buildPatsCommand() {
|
|
|
12134
12535
|
}
|
|
12135
12536
|
|
|
12136
12537
|
// src/commands/policy.ts
|
|
12137
|
-
import { Command as
|
|
12538
|
+
import { Command as Command11 } from "commander";
|
|
12138
12539
|
function buildPolicyCommand() {
|
|
12139
|
-
const policy = new
|
|
12540
|
+
const policy = new Command11("policy").description(
|
|
12140
12541
|
"Read app-level policy (read-only \u2014 edits via dashboard)"
|
|
12141
12542
|
);
|
|
12142
12543
|
policy.command("show-app").description(
|
|
@@ -12153,7 +12554,7 @@ function buildPolicyCommand() {
|
|
|
12153
12554
|
|
|
12154
12555
|
// src/commands/providers.ts
|
|
12155
12556
|
import { readFileSync as readFileSync7 } from "fs";
|
|
12156
|
-
import { Command as
|
|
12557
|
+
import { Command as Command12 } from "commander";
|
|
12157
12558
|
var CREDENTIAL_SOURCES = ["custom", "shared_dev"];
|
|
12158
12559
|
var PROVIDER_STATUSES = ["active", "disabled"];
|
|
12159
12560
|
var PROVIDER_COLUMNS = [
|
|
@@ -12232,7 +12633,7 @@ function surfaceProviderResponse(row) {
|
|
|
12232
12633
|
}
|
|
12233
12634
|
}
|
|
12234
12635
|
function buildProvidersCommand() {
|
|
12235
|
-
const providers = new
|
|
12636
|
+
const providers = new Command12("providers").description(
|
|
12236
12637
|
"Manage OAuth provider configs"
|
|
12237
12638
|
);
|
|
12238
12639
|
providers.command("list").description("List OAuth provider configs for an app").option("--app <app-id>", "App ID. Falls back to ALTER_APP_ID env or .alter/config.yaml").option(
|
|
@@ -12469,9 +12870,9 @@ function buildProvidersCommand() {
|
|
|
12469
12870
|
}
|
|
12470
12871
|
|
|
12471
12872
|
// src/commands/sdk-passthrough.ts
|
|
12472
|
-
import { Command as
|
|
12873
|
+
import { Command as Command13 } from "commander";
|
|
12473
12874
|
function buildSdkPassthroughCommand() {
|
|
12474
|
-
const cmd = new
|
|
12875
|
+
const cmd = new Command13("sdk-passthrough").description(
|
|
12475
12876
|
"Escape hatch for ad-hoc runtime SDK calls (vault.request). Uses a runtime API key, NOT a PAT."
|
|
12476
12877
|
);
|
|
12477
12878
|
cmd.command("request <grant-id>").description(
|
|
@@ -12600,7 +13001,7 @@ function buildSdkPassthroughCommand() {
|
|
|
12600
13001
|
// src/commands/self-update.ts
|
|
12601
13002
|
import { spawnSync } from "child_process";
|
|
12602
13003
|
import { platform as platform4 } from "os";
|
|
12603
|
-
import { Command as
|
|
13004
|
+
import { Command as Command14 } from "commander";
|
|
12604
13005
|
var CLI_PACKAGE = "@alter-ai/cli";
|
|
12605
13006
|
function runNpmInstall(spec) {
|
|
12606
13007
|
const result = spawnSync(
|
|
@@ -12655,7 +13056,7 @@ function probeNpmInstall() {
|
|
|
12655
13056
|
};
|
|
12656
13057
|
}
|
|
12657
13058
|
function buildSelfUpdateCommand() {
|
|
12658
|
-
return new
|
|
13059
|
+
return new Command14("self-update").description("Upgrade the alter CLI globally via npm").option("--to <version>", "Pin to a specific version instead of @latest").option(
|
|
12659
13060
|
"--dry-run",
|
|
12660
13061
|
"Print the npm install command that would run, without executing it"
|
|
12661
13062
|
).action(async (options) => {
|
|
@@ -12710,7 +13111,7 @@ function buildSelfUpdateCommand() {
|
|
|
12710
13111
|
|
|
12711
13112
|
// src/program.ts
|
|
12712
13113
|
function buildProgram() {
|
|
12713
|
-
const program2 = new
|
|
13114
|
+
const program2 = new Command15();
|
|
12714
13115
|
program2.name("alter").description("Alter Vault command-line interface").version(package_default.version);
|
|
12715
13116
|
program2.option(
|
|
12716
13117
|
"--fields <list>",
|
|
@@ -12739,6 +13140,7 @@ function buildProgram() {
|
|
|
12739
13140
|
program2.addCommand(buildManagedSecretsCommand());
|
|
12740
13141
|
program2.addCommand(buildPolicyCommand());
|
|
12741
13142
|
program2.addCommand(buildAuditCommand());
|
|
13143
|
+
program2.addCommand(buildGrantsCommand());
|
|
12742
13144
|
program2.addCommand(buildPatsCommand());
|
|
12743
13145
|
program2.addCommand(buildLinkCommand());
|
|
12744
13146
|
program2.addCommand(buildUnlinkCommand());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alter-ai/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Command-line interface for the Alter Vault dev portal — scripted dashboard automation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"tsup": "^8.0.0",
|
|
44
44
|
"tsx": "^4.7.0",
|
|
45
45
|
"typescript": "^5.5.0",
|
|
46
|
-
"vitest": "^
|
|
46
|
+
"vitest": "^4.1.8"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=20.10.0"
|