@alter-ai/cli 0.3.1 → 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/README.md +3 -2
- package/dist/cli.js +606 -130
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,8 +26,9 @@ apps list | create | show | update | archive | unarchive | delete
|
|
|
26
26
|
keys list | mint | show | rotate | revoke | rename
|
|
27
27
|
agents list | create | show | update | revoke (+ mint-key, list-keys, revoke-key)
|
|
28
28
|
providers list | list-catalog | create | show | update | delete
|
|
29
|
-
managed-secrets list | show | create | rotate | delete | templates | access |
|
|
30
|
-
|
|
29
|
+
managed-secrets list | show | create | rotate | delete | templates | access |
|
|
30
|
+
set-delegation-policy | users |
|
|
31
|
+
grants {list, list-for-agent, create, update, revoke} |
|
|
31
32
|
groups {list, show}
|
|
32
33
|
policy show-app
|
|
33
34
|
audit list | show | portal-actions | grant-events | traces
|
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");
|
|
5310
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
|
+
);
|
|
5496
|
+
}
|
|
5497
|
+
this.#userTokenGetter = () => result.userToken;
|
|
5498
|
+
return result;
|
|
5311
5499
|
}
|
|
5312
5500
|
/**
|
|
5313
5501
|
* Poll the Connect session for completion status (INTERNAL).
|
|
@@ -6229,6 +6417,21 @@ var Agent = class _Agent {
|
|
|
6229
6417
|
async connect(options) {
|
|
6230
6418
|
return this.#client.connect(options);
|
|
6231
6419
|
}
|
|
6420
|
+
/**
|
|
6421
|
+
* Poll a Connect session to completion. See
|
|
6422
|
+
* {@link _VaultClient.pollConnectSession} for the full signature.
|
|
6423
|
+
*/
|
|
6424
|
+
async pollConnectSession(...args) {
|
|
6425
|
+
return this.#client.pollConnectSession(...args);
|
|
6426
|
+
}
|
|
6427
|
+
/**
|
|
6428
|
+
* Mint a recovery Connect session from a typed error. See
|
|
6429
|
+
* {@link _VaultClient.createConnectSessionForError} for the full
|
|
6430
|
+
* signature.
|
|
6431
|
+
*/
|
|
6432
|
+
async createConnectSessionForError(...args) {
|
|
6433
|
+
return this.#client.createConnectSessionForError(...args);
|
|
6434
|
+
}
|
|
6232
6435
|
// ── Delegation self-revoke ─────────────────────────────────────────────
|
|
6233
6436
|
/**
|
|
6234
6437
|
* Opt out of this agent's delegation on an OAuth grant.
|
|
@@ -6408,7 +6611,36 @@ var App = class _App {
|
|
|
6408
6611
|
async connect(options) {
|
|
6409
6612
|
return this.#client.connect(options);
|
|
6410
6613
|
}
|
|
6614
|
+
/**
|
|
6615
|
+
* Poll a Connect session to completion. See
|
|
6616
|
+
* {@link _VaultClient.pollConnectSession} for the full signature.
|
|
6617
|
+
*/
|
|
6618
|
+
async pollConnectSession(...args) {
|
|
6619
|
+
return this.#client.pollConnectSession(...args);
|
|
6620
|
+
}
|
|
6621
|
+
/**
|
|
6622
|
+
* Mint a recovery Connect session from a typed error. See
|
|
6623
|
+
* {@link _VaultClient.createConnectSessionForError} for the full
|
|
6624
|
+
* signature.
|
|
6625
|
+
*/
|
|
6626
|
+
async createConnectSessionForError(...args) {
|
|
6627
|
+
return this.#client.createConnectSessionForError(...args);
|
|
6628
|
+
}
|
|
6411
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
|
+
}
|
|
6412
6644
|
async authenticate(options) {
|
|
6413
6645
|
return this.#client.authenticate(options);
|
|
6414
6646
|
}
|
|
@@ -6841,7 +7073,7 @@ var DEFAULT_BASE_URL = "https://backend.alterauth.com";
|
|
|
6841
7073
|
var PAT_API_PREFIX = "/api/v1/dev-portal";
|
|
6842
7074
|
var HTTP_ERROR_THRESHOLD = 400;
|
|
6843
7075
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
6844
|
-
var CLI_VERSION = "0.3.
|
|
7076
|
+
var CLI_VERSION = "0.3.3";
|
|
6845
7077
|
var USER_AGENT = buildUserAgent();
|
|
6846
7078
|
function buildUserAgent() {
|
|
6847
7079
|
let osTag = "";
|
|
@@ -6987,6 +7219,9 @@ var DashboardClient = class {
|
|
|
6987
7219
|
policy;
|
|
6988
7220
|
/** Audit log read surface (CLI `alter audit` subcommands). */
|
|
6989
7221
|
audit;
|
|
7222
|
+
/** OAuth + managed-secret grant inspect/revoke (CLI `alter grants`
|
|
7223
|
+
* subcommands). Requires ``dashboard_grants:read`` / ``:admin``. */
|
|
7224
|
+
grants;
|
|
6990
7225
|
#closed = false;
|
|
6991
7226
|
constructor(options = {}) {
|
|
6992
7227
|
const resolvedPat = options.pat ?? (typeof process !== "undefined" ? process.env.ALTER_PAT : void 0);
|
|
@@ -7036,6 +7271,7 @@ var DashboardClient = class {
|
|
|
7036
7271
|
this.managedSecrets = new ManagedSecretsNamespace(this);
|
|
7037
7272
|
this.policy = new PolicyNamespace(this);
|
|
7038
7273
|
this.audit = new AuditNamespace(this);
|
|
7274
|
+
this.grants = new GrantsNamespace(this);
|
|
7039
7275
|
}
|
|
7040
7276
|
/**
|
|
7041
7277
|
* Release internal state. The default `fetch` is a global; this is
|
|
@@ -7999,9 +8235,8 @@ var AuditNamespace = class {
|
|
|
7999
8235
|
}
|
|
8000
8236
|
/** List runtime audit-log rows. Requires `dashboard_audit:read`.
|
|
8001
8237
|
*
|
|
8002
|
-
*
|
|
8003
|
-
*
|
|
8004
|
-
* ``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).
|
|
8005
8240
|
*/
|
|
8006
8241
|
async list(options = {}) {
|
|
8007
8242
|
const query = optionsToQuery({
|
|
@@ -8014,8 +8249,8 @@ var AuditNamespace = class {
|
|
|
8014
8249
|
}
|
|
8015
8250
|
/** List dashboard / CLI admin actions. Requires `dashboard_audit:read`.
|
|
8016
8251
|
*
|
|
8017
|
-
*
|
|
8018
|
-
* ``
|
|
8252
|
+
* Returns the canonical envelope ``{items, total, limit, offset, has_more}``
|
|
8253
|
+
* plus the distinct ``is_truncated`` cap-signal.
|
|
8019
8254
|
*/
|
|
8020
8255
|
async listPortalActions(options = {}) {
|
|
8021
8256
|
const query = optionsToQuery({
|
|
@@ -8033,8 +8268,8 @@ var AuditNamespace = class {
|
|
|
8033
8268
|
}
|
|
8034
8269
|
/** List grant-lifecycle events. Requires `dashboard_audit:read`.
|
|
8035
8270
|
*
|
|
8036
|
-
*
|
|
8037
|
-
* ``
|
|
8271
|
+
* Returns the canonical envelope ``{items, total, limit, offset, has_more}``
|
|
8272
|
+
* plus the distinct ``is_truncated`` cap-signal.
|
|
8038
8273
|
*/
|
|
8039
8274
|
async listGrantEvents(options = {}) {
|
|
8040
8275
|
const query = optionsToQuery({
|
|
@@ -8052,8 +8287,8 @@ var AuditNamespace = class {
|
|
|
8052
8287
|
}
|
|
8053
8288
|
/** List trace summaries. Requires `dashboard_audit:read`.
|
|
8054
8289
|
*
|
|
8055
|
-
*
|
|
8056
|
-
* ``
|
|
8290
|
+
* Returns the canonical envelope ``{items, total, limit, offset, has_more}``
|
|
8291
|
+
* plus the distinct ``is_event_window_capped`` signal.
|
|
8057
8292
|
*/
|
|
8058
8293
|
async listTraces(options = {}) {
|
|
8059
8294
|
const query = optionsToQuery({
|
|
@@ -8080,6 +8315,59 @@ var AuditNamespace = class {
|
|
|
8080
8315
|
return expectDict(body, "audit.get_trace", 200);
|
|
8081
8316
|
}
|
|
8082
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
|
+
};
|
|
8083
8371
|
|
|
8084
8372
|
// src/portal-factory.ts
|
|
8085
8373
|
async function createPortalClient() {
|
|
@@ -9269,10 +9557,6 @@ function parseRelativeFromNow(raw, direction) {
|
|
|
9269
9557
|
}
|
|
9270
9558
|
|
|
9271
9559
|
// src/commands/audit.ts
|
|
9272
|
-
function normalizeAuditEnvelope(result, domainKey) {
|
|
9273
|
-
const { [domainKey]: items, ...rest } = result;
|
|
9274
|
-
return { ...rest, items };
|
|
9275
|
-
}
|
|
9276
9560
|
function resolveStartDate(start, since) {
|
|
9277
9561
|
if (start !== void 0 && since !== void 0) {
|
|
9278
9562
|
process.stderr.write(
|
|
@@ -9385,7 +9669,7 @@ function buildAuditCommand() {
|
|
|
9385
9669
|
offset: options.offset
|
|
9386
9670
|
});
|
|
9387
9671
|
const logs = validateRows(
|
|
9388
|
-
extractArrayField(result, "
|
|
9672
|
+
extractArrayField(result, "items", "audit.list"),
|
|
9389
9673
|
{
|
|
9390
9674
|
id: isString,
|
|
9391
9675
|
timestamp: isString,
|
|
@@ -9404,7 +9688,7 @@ function buildAuditCommand() {
|
|
|
9404
9688
|
if (format === "table" || format === "jsonl") {
|
|
9405
9689
|
emit(format, logs, AUDIT_COLUMNS);
|
|
9406
9690
|
} else {
|
|
9407
|
-
emit(format,
|
|
9691
|
+
emit(format, result);
|
|
9408
9692
|
}
|
|
9409
9693
|
});
|
|
9410
9694
|
}
|
|
@@ -9451,7 +9735,7 @@ function buildAuditCommand() {
|
|
|
9451
9735
|
offset: options.offset
|
|
9452
9736
|
});
|
|
9453
9737
|
const events = validateRows(
|
|
9454
|
-
extractArrayField(result, "
|
|
9738
|
+
extractArrayField(result, "items", "audit.list_portal_actions"),
|
|
9455
9739
|
{
|
|
9456
9740
|
id: isString,
|
|
9457
9741
|
timestamp: isString,
|
|
@@ -9466,7 +9750,7 @@ function buildAuditCommand() {
|
|
|
9466
9750
|
if (format === "table" || format === "jsonl") {
|
|
9467
9751
|
emit(format, events, PORTAL_ACTION_COLUMNS);
|
|
9468
9752
|
} else {
|
|
9469
|
-
emit(format,
|
|
9753
|
+
emit(format, result);
|
|
9470
9754
|
}
|
|
9471
9755
|
});
|
|
9472
9756
|
}
|
|
@@ -9502,7 +9786,7 @@ function buildAuditCommand() {
|
|
|
9502
9786
|
offset: options.offset
|
|
9503
9787
|
});
|
|
9504
9788
|
const events = validateRows(
|
|
9505
|
-
extractArrayField(result, "
|
|
9789
|
+
extractArrayField(result, "items", "audit.list_grant_events"),
|
|
9506
9790
|
{
|
|
9507
9791
|
id: isString,
|
|
9508
9792
|
timestamp: isString,
|
|
@@ -9516,7 +9800,7 @@ function buildAuditCommand() {
|
|
|
9516
9800
|
if (format === "table" || format === "jsonl") {
|
|
9517
9801
|
emit(format, events, GRANT_EVENT_COLUMNS);
|
|
9518
9802
|
} else {
|
|
9519
|
-
emit(format,
|
|
9803
|
+
emit(format, result);
|
|
9520
9804
|
}
|
|
9521
9805
|
});
|
|
9522
9806
|
}
|
|
@@ -9543,7 +9827,7 @@ function buildAuditCommand() {
|
|
|
9543
9827
|
offset: options.offset
|
|
9544
9828
|
});
|
|
9545
9829
|
const traces = validateRows(
|
|
9546
|
-
extractArrayField(result, "
|
|
9830
|
+
extractArrayField(result, "items", "audit.list_traces"),
|
|
9547
9831
|
{
|
|
9548
9832
|
trace_id: isString,
|
|
9549
9833
|
first_timestamp: isString,
|
|
@@ -9558,7 +9842,7 @@ function buildAuditCommand() {
|
|
|
9558
9842
|
if (format === "table" || format === "jsonl") {
|
|
9559
9843
|
emit(format, traces, TRACE_COLUMNS);
|
|
9560
9844
|
} else {
|
|
9561
|
-
emit(format,
|
|
9845
|
+
emit(format, result);
|
|
9562
9846
|
}
|
|
9563
9847
|
});
|
|
9564
9848
|
}
|
|
@@ -9600,7 +9884,13 @@ var DEFAULT_SCOPES = [
|
|
|
9600
9884
|
// ``:delete`` and ``:admin`` are deliberate opt-ins via ``--scopes``
|
|
9601
9885
|
// per the destructive-action policy in CLAUDE.md.
|
|
9602
9886
|
"dashboard_secrets:read",
|
|
9603
|
-
"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"
|
|
9604
9894
|
];
|
|
9605
9895
|
function deriveDashboardUrl(baseUrl) {
|
|
9606
9896
|
return baseUrl.replace(/^https:\/\/backend\./, "https://portal.");
|
|
@@ -9890,7 +10180,17 @@ var DASHBOARD_RESOURCE_VERBS = {
|
|
|
9890
10180
|
// grant + delegation + audit anchor and vault-deletes the stored
|
|
9891
10181
|
// credential (irrecoverable). Backend catalog v3, see
|
|
9892
10182
|
// ``apps/backend/app/core/scopes/catalog.py`` _DASHBOARD_RESOURCES_V3.
|
|
9893
|
-
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"]
|
|
9894
10194
|
};
|
|
9895
10195
|
function parseScopesFlag(raw) {
|
|
9896
10196
|
if (typeof raw !== "string") {
|
|
@@ -10538,8 +10838,148 @@ function buildCompletionCommand() {
|
|
|
10538
10838
|
return completion;
|
|
10539
10839
|
}
|
|
10540
10840
|
|
|
10541
|
-
// src/commands/
|
|
10841
|
+
// src/commands/grants.ts
|
|
10542
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";
|
|
10543
10983
|
var KEY_TYPE_MAP = {
|
|
10544
10984
|
runtime: "rk",
|
|
10545
10985
|
agent: "ak"
|
|
@@ -10553,7 +10993,7 @@ var KEY_COLUMNS2 = [
|
|
|
10553
10993
|
{ label: "STATUS", get: (k) => k.revoked_at ? "revoked" : "active" }
|
|
10554
10994
|
];
|
|
10555
10995
|
function buildKeysCommand() {
|
|
10556
|
-
const keys = new
|
|
10996
|
+
const keys = new Command7("keys").description("Manage scoped API keys");
|
|
10557
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(
|
|
10558
10998
|
"--output <format>",
|
|
10559
10999
|
"Output format: json|jsonl|table (default: table)",
|
|
@@ -10775,9 +11215,9 @@ function buildKeysCommand() {
|
|
|
10775
11215
|
// src/commands/link.ts
|
|
10776
11216
|
import { existsSync as existsSync4 } from "fs";
|
|
10777
11217
|
import { join as join5 } from "path";
|
|
10778
|
-
import { Command as
|
|
11218
|
+
import { Command as Command8 } from "commander";
|
|
10779
11219
|
function buildLinkCommand() {
|
|
10780
|
-
const link = new
|
|
11220
|
+
const link = new Command8("link").description(
|
|
10781
11221
|
"Pin the current directory tree to a default app_id so subsequent commands (``keys``, ``agents``, ``providers``, ``policy``) don't need --app."
|
|
10782
11222
|
).argument("[app-id]", "App ID to pin. Omitted when --status is passed.").option(
|
|
10783
11223
|
"--force",
|
|
@@ -10805,7 +11245,7 @@ function buildLinkCommand() {
|
|
|
10805
11245
|
return link;
|
|
10806
11246
|
}
|
|
10807
11247
|
function buildUnlinkCommand() {
|
|
10808
|
-
return new
|
|
11248
|
+
return new Command8("unlink").description("Remove the current directory's .alter/config.yaml pin.").action(async () => {
|
|
10809
11249
|
const removed = removeWorkspaceConfig();
|
|
10810
11250
|
if (removed) {
|
|
10811
11251
|
process.stdout.write(
|
|
@@ -10893,7 +11333,7 @@ async function runStatus() {
|
|
|
10893
11333
|
|
|
10894
11334
|
// src/commands/managed-secrets.ts
|
|
10895
11335
|
import { readFileSync as readFileSync6 } from "fs";
|
|
10896
|
-
import { Command as
|
|
11336
|
+
import { Command as Command9, Option } from "commander";
|
|
10897
11337
|
var PRINCIPAL_TYPES = ["user", "group", "system", "agent"];
|
|
10898
11338
|
var CREDENTIAL_TYPES = [
|
|
10899
11339
|
"bearer_token",
|
|
@@ -11247,7 +11687,7 @@ var SECRET_COLUMNS = [
|
|
|
11247
11687
|
{ label: "GRANTS", get: (s) => String(s.grant_count) },
|
|
11248
11688
|
{ label: "CREATED", get: (s) => s.created_at }
|
|
11249
11689
|
];
|
|
11250
|
-
var
|
|
11690
|
+
var GRANT_COLUMNS2 = [
|
|
11251
11691
|
{ label: "GRANT_ID", get: (g) => g.grant_id, maxWidth: 36 },
|
|
11252
11692
|
{ label: "PRINCIPAL", get: (g) => g.principal_type },
|
|
11253
11693
|
{ label: "LABEL", get: (g) => g.label ?? "\u2014", maxWidth: 24 },
|
|
@@ -11279,7 +11719,7 @@ var ACCESS_COLUMNS = [
|
|
|
11279
11719
|
{ label: "AGENT", get: (a) => a.via_agent_name ?? "\u2014" }
|
|
11280
11720
|
];
|
|
11281
11721
|
function buildGrantsSubcommand() {
|
|
11282
|
-
const grants = new
|
|
11722
|
+
const grants = new Command9("grants").description(
|
|
11283
11723
|
"Manage grants on managed secrets (list / create / update / revoke)."
|
|
11284
11724
|
);
|
|
11285
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(
|
|
@@ -11317,7 +11757,7 @@ function buildGrantsSubcommand() {
|
|
|
11317
11757
|
created_at: isString,
|
|
11318
11758
|
expires_at: isOptionalString
|
|
11319
11759
|
}, "managed-secrets.grants.list");
|
|
11320
|
-
emit(format, rows,
|
|
11760
|
+
emit(format, rows, GRANT_COLUMNS2);
|
|
11321
11761
|
});
|
|
11322
11762
|
}
|
|
11323
11763
|
);
|
|
@@ -11347,7 +11787,7 @@ function buildGrantsSubcommand() {
|
|
|
11347
11787
|
created_at: isString,
|
|
11348
11788
|
expires_at: isOptionalString
|
|
11349
11789
|
}, "managed-secrets.grants.list-for-agent");
|
|
11350
|
-
emit(format, rows,
|
|
11790
|
+
emit(format, rows, GRANT_COLUMNS2);
|
|
11351
11791
|
});
|
|
11352
11792
|
}
|
|
11353
11793
|
);
|
|
@@ -11551,7 +11991,7 @@ function buildGrantsSubcommand() {
|
|
|
11551
11991
|
return grants;
|
|
11552
11992
|
}
|
|
11553
11993
|
function buildGroupsSubcommand() {
|
|
11554
|
-
const groups = new
|
|
11994
|
+
const groups = new Command9("groups").description(
|
|
11555
11995
|
"App-user-group autocomplete + group detail helpers."
|
|
11556
11996
|
);
|
|
11557
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(
|
|
@@ -11623,7 +12063,7 @@ function buildGroupsSubcommand() {
|
|
|
11623
12063
|
return groups;
|
|
11624
12064
|
}
|
|
11625
12065
|
function buildManagedSecretsCommand() {
|
|
11626
|
-
const root = new
|
|
12066
|
+
const root = new Command9("managed-secrets").description(
|
|
11627
12067
|
"Manage managed-secret credentials, grants, and access (CRUD + autocomplete helpers)."
|
|
11628
12068
|
);
|
|
11629
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(
|
|
@@ -11694,7 +12134,7 @@ function buildManagedSecretsCommand() {
|
|
|
11694
12134
|
root.command("create").description("Create a managed secret + base grant").option("--app <app-id>", "App ID").option(
|
|
11695
12135
|
"--input <path>",
|
|
11696
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."
|
|
11697
|
-
).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(
|
|
11698
12138
|
// ``--credential-type`` is enum-validated at RUNTIME (see the
|
|
11699
12139
|
// per-field branch below) rather than parse-time so a typo'd
|
|
11700
12140
|
// value alongside ``--input @body.json`` (which discards the
|
|
@@ -11739,7 +12179,6 @@ function buildManagedSecretsCommand() {
|
|
|
11739
12179
|
"--input"
|
|
11740
12180
|
);
|
|
11741
12181
|
const perField = [
|
|
11742
|
-
"slug",
|
|
11743
12182
|
"name",
|
|
11744
12183
|
"description",
|
|
11745
12184
|
"template",
|
|
@@ -11772,12 +12211,6 @@ function buildManagedSecretsCommand() {
|
|
|
11772
12211
|
});
|
|
11773
12212
|
return;
|
|
11774
12213
|
}
|
|
11775
|
-
if (options.slug === void 0) {
|
|
11776
|
-
process.stderr.write(
|
|
11777
|
-
"alter: managed-secrets create requires --slug (or use --input @file.json to supply the full body)\n"
|
|
11778
|
-
);
|
|
11779
|
-
process.exit(EXIT_USAGE);
|
|
11780
|
-
}
|
|
11781
12214
|
if (options.name === void 0) {
|
|
11782
12215
|
process.stderr.write(
|
|
11783
12216
|
"alter: managed-secrets create requires --name (or use --input @file.json to supply the full body)\n"
|
|
@@ -11790,7 +12223,6 @@ function buildManagedSecretsCommand() {
|
|
|
11790
12223
|
CREDENTIAL_TYPES
|
|
11791
12224
|
);
|
|
11792
12225
|
validateNoControlCharsOrExit("--name", options.name);
|
|
11793
|
-
validateNoControlCharsOrExit("--slug", options.slug);
|
|
11794
12226
|
if (options.description !== void 0) {
|
|
11795
12227
|
validateNoControlCharsOrExit("--description", options.description);
|
|
11796
12228
|
}
|
|
@@ -11814,7 +12246,6 @@ function buildManagedSecretsCommand() {
|
|
|
11814
12246
|
const additionalCredentials = loadedCredentials !== void 0 ? loadedCredentials.additionalCredentials : options.credentialField;
|
|
11815
12247
|
const additionalInjections = options.injectionRule !== void 0 ? loadInjectionRules(options.injectionRule) : void 0;
|
|
11816
12248
|
const body = {
|
|
11817
|
-
slug: options.slug,
|
|
11818
12249
|
name: options.name,
|
|
11819
12250
|
credential_type: options.credentialType,
|
|
11820
12251
|
credential_value: credentialValue
|
|
@@ -12073,7 +12504,7 @@ function buildManagedSecretsCommand() {
|
|
|
12073
12504
|
}
|
|
12074
12505
|
|
|
12075
12506
|
// src/commands/pats.ts
|
|
12076
|
-
import { Command as
|
|
12507
|
+
import { Command as Command10 } from "commander";
|
|
12077
12508
|
async function whoami(options) {
|
|
12078
12509
|
const client = await createPortalClient();
|
|
12079
12510
|
let exitCode = 0;
|
|
@@ -12095,7 +12526,7 @@ async function whoami(options) {
|
|
|
12095
12526
|
}
|
|
12096
12527
|
}
|
|
12097
12528
|
function buildPatsCommand() {
|
|
12098
|
-
const pats = new
|
|
12529
|
+
const pats = new Command10("pats").description("Inspect Personal Access Tokens");
|
|
12099
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) => {
|
|
12100
12531
|
const format = coerceOutputFormat(options.output);
|
|
12101
12532
|
await whoami({ output: format });
|
|
@@ -12104,9 +12535,9 @@ function buildPatsCommand() {
|
|
|
12104
12535
|
}
|
|
12105
12536
|
|
|
12106
12537
|
// src/commands/policy.ts
|
|
12107
|
-
import { Command as
|
|
12538
|
+
import { Command as Command11 } from "commander";
|
|
12108
12539
|
function buildPolicyCommand() {
|
|
12109
|
-
const policy = new
|
|
12540
|
+
const policy = new Command11("policy").description(
|
|
12110
12541
|
"Read app-level policy (read-only \u2014 edits via dashboard)"
|
|
12111
12542
|
);
|
|
12112
12543
|
policy.command("show-app").description(
|
|
@@ -12123,7 +12554,7 @@ function buildPolicyCommand() {
|
|
|
12123
12554
|
|
|
12124
12555
|
// src/commands/providers.ts
|
|
12125
12556
|
import { readFileSync as readFileSync7 } from "fs";
|
|
12126
|
-
import { Command as
|
|
12557
|
+
import { Command as Command12 } from "commander";
|
|
12127
12558
|
var CREDENTIAL_SOURCES = ["custom", "shared_dev"];
|
|
12128
12559
|
var PROVIDER_STATUSES = ["active", "disabled"];
|
|
12129
12560
|
var PROVIDER_COLUMNS = [
|
|
@@ -12161,8 +12592,48 @@ function collectRedirectUris(value, previous = []) {
|
|
|
12161
12592
|
const next = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
12162
12593
|
return [...previous, ...next];
|
|
12163
12594
|
}
|
|
12595
|
+
function sanitizeStderrText(value) {
|
|
12596
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ").trim();
|
|
12597
|
+
}
|
|
12598
|
+
function surfaceProviderResponse(row) {
|
|
12599
|
+
if (typeof row !== "object" || row === null) return;
|
|
12600
|
+
const r = row;
|
|
12601
|
+
const preflight = r.preflight;
|
|
12602
|
+
if (preflight && typeof preflight === "object") {
|
|
12603
|
+
const status = preflight.status;
|
|
12604
|
+
const msg = typeof preflight.message === "string" ? sanitizeStderrText(preflight.message) : "";
|
|
12605
|
+
if (status === "passed") {
|
|
12606
|
+
process.stderr.write("alter: preflight: passed\n");
|
|
12607
|
+
} else if (status === "failed") {
|
|
12608
|
+
process.stderr.write(
|
|
12609
|
+
`alter: preflight: failed${msg ? ` \u2014 ${msg}` : ""}
|
|
12610
|
+
`
|
|
12611
|
+
);
|
|
12612
|
+
} else if (status === "inconclusive") {
|
|
12613
|
+
process.stderr.write(
|
|
12614
|
+
`alter: preflight: inconclusive${msg ? ` \u2014 ${msg}` : ""} (save was allowed)
|
|
12615
|
+
`
|
|
12616
|
+
);
|
|
12617
|
+
}
|
|
12618
|
+
}
|
|
12619
|
+
const bc = r.breaking_changes;
|
|
12620
|
+
if (Array.isArray(bc) && bc.length > 0) {
|
|
12621
|
+
process.stderr.write(
|
|
12622
|
+
`alter: warning: ${bc.length} breaking change(s) \u2014 ${sanitizeStderrText(JSON.stringify(bc))}
|
|
12623
|
+
`
|
|
12624
|
+
);
|
|
12625
|
+
}
|
|
12626
|
+
const warning = r.warning;
|
|
12627
|
+
if (typeof warning === "string") {
|
|
12628
|
+
const sanitized = sanitizeStderrText(warning);
|
|
12629
|
+
if (sanitized.length > 0) {
|
|
12630
|
+
process.stderr.write(`alter: warning: ${sanitized}
|
|
12631
|
+
`);
|
|
12632
|
+
}
|
|
12633
|
+
}
|
|
12634
|
+
}
|
|
12164
12635
|
function buildProvidersCommand() {
|
|
12165
|
-
const providers = new
|
|
12636
|
+
const providers = new Command12("providers").description(
|
|
12166
12637
|
"Manage OAuth provider configs"
|
|
12167
12638
|
);
|
|
12168
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(
|
|
@@ -12251,6 +12722,7 @@ function buildProvidersCommand() {
|
|
|
12251
12722
|
appId,
|
|
12252
12723
|
body
|
|
12253
12724
|
);
|
|
12725
|
+
surfaceProviderResponse(row);
|
|
12254
12726
|
emit(format, row);
|
|
12255
12727
|
});
|
|
12256
12728
|
return;
|
|
@@ -12290,6 +12762,7 @@ function buildProvidersCommand() {
|
|
|
12290
12762
|
scopes,
|
|
12291
12763
|
redirect_uris: redirects
|
|
12292
12764
|
});
|
|
12765
|
+
surfaceProviderResponse(row);
|
|
12293
12766
|
emit(format, row);
|
|
12294
12767
|
});
|
|
12295
12768
|
}
|
|
@@ -12336,6 +12809,7 @@ function buildProvidersCommand() {
|
|
|
12336
12809
|
options.provider,
|
|
12337
12810
|
body
|
|
12338
12811
|
);
|
|
12812
|
+
surfaceProviderResponse(row);
|
|
12339
12813
|
emit(format, row);
|
|
12340
12814
|
});
|
|
12341
12815
|
return;
|
|
@@ -12362,6 +12836,7 @@ function buildProvidersCommand() {
|
|
|
12362
12836
|
redirect_uris: redirects,
|
|
12363
12837
|
status: providerStatus
|
|
12364
12838
|
});
|
|
12839
|
+
surfaceProviderResponse(row);
|
|
12365
12840
|
emit(format, row);
|
|
12366
12841
|
});
|
|
12367
12842
|
}
|
|
@@ -12395,9 +12870,9 @@ function buildProvidersCommand() {
|
|
|
12395
12870
|
}
|
|
12396
12871
|
|
|
12397
12872
|
// src/commands/sdk-passthrough.ts
|
|
12398
|
-
import { Command as
|
|
12873
|
+
import { Command as Command13 } from "commander";
|
|
12399
12874
|
function buildSdkPassthroughCommand() {
|
|
12400
|
-
const cmd = new
|
|
12875
|
+
const cmd = new Command13("sdk-passthrough").description(
|
|
12401
12876
|
"Escape hatch for ad-hoc runtime SDK calls (vault.request). Uses a runtime API key, NOT a PAT."
|
|
12402
12877
|
);
|
|
12403
12878
|
cmd.command("request <grant-id>").description(
|
|
@@ -12526,7 +13001,7 @@ function buildSdkPassthroughCommand() {
|
|
|
12526
13001
|
// src/commands/self-update.ts
|
|
12527
13002
|
import { spawnSync } from "child_process";
|
|
12528
13003
|
import { platform as platform4 } from "os";
|
|
12529
|
-
import { Command as
|
|
13004
|
+
import { Command as Command14 } from "commander";
|
|
12530
13005
|
var CLI_PACKAGE = "@alter-ai/cli";
|
|
12531
13006
|
function runNpmInstall(spec) {
|
|
12532
13007
|
const result = spawnSync(
|
|
@@ -12581,7 +13056,7 @@ function probeNpmInstall() {
|
|
|
12581
13056
|
};
|
|
12582
13057
|
}
|
|
12583
13058
|
function buildSelfUpdateCommand() {
|
|
12584
|
-
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(
|
|
12585
13060
|
"--dry-run",
|
|
12586
13061
|
"Print the npm install command that would run, without executing it"
|
|
12587
13062
|
).action(async (options) => {
|
|
@@ -12636,7 +13111,7 @@ function buildSelfUpdateCommand() {
|
|
|
12636
13111
|
|
|
12637
13112
|
// src/program.ts
|
|
12638
13113
|
function buildProgram() {
|
|
12639
|
-
const program2 = new
|
|
13114
|
+
const program2 = new Command15();
|
|
12640
13115
|
program2.name("alter").description("Alter Vault command-line interface").version(package_default.version);
|
|
12641
13116
|
program2.option(
|
|
12642
13117
|
"--fields <list>",
|
|
@@ -12665,6 +13140,7 @@ function buildProgram() {
|
|
|
12665
13140
|
program2.addCommand(buildManagedSecretsCommand());
|
|
12666
13141
|
program2.addCommand(buildPolicyCommand());
|
|
12667
13142
|
program2.addCommand(buildAuditCommand());
|
|
13143
|
+
program2.addCommand(buildGrantsCommand());
|
|
12668
13144
|
program2.addCommand(buildPatsCommand());
|
|
12669
13145
|
program2.addCommand(buildLinkCommand());
|
|
12670
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"
|