@alter-ai/cli 0.1.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -188
- package/dist/cli.js +364 -72
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,208 +1,45 @@
|
|
|
1
|
-
# `@alter-ai/cli`
|
|
1
|
+
# `@alter-ai/cli`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command-line client for the Alter Vault dev portal. Authenticates with a Personal Access Token (PAT) and exposes the dashboard's resource model as scriptable commands.
|
|
4
|
+
|
|
5
|
+
📖 **Full docs:** [docs.alterauth.com/reference/cli](https://docs.alterauth.com/reference/cli)
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
10
|
npm install -g @alter-ai/cli
|
|
9
|
-
# verify
|
|
10
11
|
alter --version
|
|
11
12
|
```
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
The fastest interactive sign-in is the browser-dance flow — `alter auth login` opens the dashboard, you click **Authorize**, and the CLI receives the freshly-minted token on a localhost listener:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
alter auth login
|
|
19
|
-
# alter: opening browser at https://dashboard.alterauth.com/cli-auth
|
|
20
|
-
# (waiting up to 2 minutes for you to approve)…
|
|
21
|
-
# alter: signed in via browser-dance flow.
|
|
22
|
-
|
|
23
|
-
alter auth status
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
For headless / CI environments, mint a PAT manually from the dashboard (**Settings → Personal Access Tokens → New token**) and feed it to the CLI through one of the channels below:
|
|
27
|
-
|
|
28
|
-
**Token file (recommended for local headless use):** keeps the value out of `process.argv` (visible to other users via `ps`) and out of shell history.
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
umask 077 && echo "alter_pat_xxxxxxxxxxxxxxxxxxxxxxxx_yyyyyy" > ~/alter-pat.txt
|
|
32
|
-
alter auth login --token-file ~/alter-pat.txt
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
**Stdin:**
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
pbpaste | alter auth login --token-stdin # macOS
|
|
39
|
-
xclip -o -selection clipboard | alter auth login --token-stdin # Linux
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**Env var (CI):** skip persistence entirely. The SDK reads `ALTER_PAT` directly; it never lands in argv.
|
|
14
|
+
## Sign in
|
|
43
15
|
|
|
44
16
|
```bash
|
|
45
|
-
|
|
46
|
-
alter apps list
|
|
17
|
+
alter auth login # interactive browser flow
|
|
18
|
+
ALTER_PAT=alter_pat_... alter apps list # headless / CI
|
|
47
19
|
```
|
|
48
20
|
|
|
49
|
-
**Inline `--token <pat>` is supported but discouraged** — the value lands in `process.argv` (visible to other local users via `ps aux`) and shell history. Use only on single-user machines and rotate afterwards.
|
|
50
|
-
|
|
51
21
|
## Commands
|
|
52
22
|
|
|
53
|
-
The CLI mirrors the dashboard's resource model. Every namespace lives at `alter <namespace> <verb>`:
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
auth login | status | logout Sign in / out + token introspection
|
|
57
|
-
apps list | create | show | update | delete Manage applications
|
|
58
|
-
keys list | mint | show | rotate | revoke | rename Manage runtime API keys
|
|
59
|
-
agents list | create | show | update | revoke Managed-agent identities (+ mint-key, list-keys, revoke-key)
|
|
60
|
-
providers list | list-catalog | create | show | update | delete OAuth provider integrations per app
|
|
61
|
-
managed-secrets
|
|
62
|
-
templates | list | show | create | delete | rotate | access | users |
|
|
63
|
-
grants {list, list-for-agent, create, update, revoke} |
|
|
64
|
-
groups {list, show} Managed-secret credentials, grants, and access (CRUD + autocomplete helpers)
|
|
65
|
-
policy show-app View app-level policy (org-wide policy is dashboard-only)
|
|
66
|
-
audit list | show | portal-actions | grant-events | traces Dev-portal audit log
|
|
67
|
-
pats whoami Same as `auth status`, under the `pats` namespace
|
|
68
|
-
link <app-id> | --status Pin an app to the current directory tree
|
|
69
|
-
unlink Clear the workspace pin
|
|
70
|
-
completion install | print Generate shell completions (bash/zsh/fish)
|
|
71
|
-
self-update --to <v> | --dry-run Upgrade the CLI via npm
|
|
72
|
-
sdk-passthrough request <method> <path> [...] Raw authenticated request (escape hatch for routes the CLI doesn't model)
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
PAT lifecycle (mint, revoke) remains **dashboard-only** in v1 — a PAT cannot mint or revoke another PAT, including its own. Operators use the dashboard for those actions. Org-wide policy and identity-provider configuration are also dashboard-only per the destructive-action policy in CLAUDE.md.
|
|
76
|
-
|
|
77
|
-
Every list / show command accepts `--output=json|table|jsonl` (default: `json` for pipelines, `table` for interactive). Object commands default to `json`. Use `--fields a,b,c` at the top level to project to specific keys (see [Field selection](#field-selection---fields)).
|
|
78
|
-
|
|
79
|
-
## Authentication
|
|
80
|
-
|
|
81
|
-
The CLI resolves credentials in this order (highest precedence first):
|
|
82
|
-
|
|
83
|
-
1. **`ALTER_PAT` environment variable** — canonical CI / headless source.
|
|
84
|
-
2. **OS keychain** (macOS Keychain, Linux Secret Service / `gnome-keyring`, Windows Credential Manager) via the optional `keytar` native module. This is the default location after a successful `alter auth login`.
|
|
85
|
-
3. **Plaintext file** `~/.config/alter/auth.toml` (XDG-compliant, mode `0600`). Used as the fallback when `keytar` failed to build on the host (e.g. missing `libsecret-1-dev` on Linux). The CLI prints a warning at login time when it falls back to this path.
|
|
86
|
-
|
|
87
|
-
The `--base-url` flag and the `ALTER_BASE_URL` environment variable both require an `https://` URL — non-HTTPS schemes (`http`, `file`, `gopher`, etc.) are rejected at login time so a misconfigured backend URL cannot exfiltrate the PAT in clear text or to an unintended target.
|
|
88
|
-
|
|
89
|
-
If keytar isn't loading on your host, install the native build tools and re-install:
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
# macOS
|
|
93
|
-
xcode-select --install
|
|
94
|
-
# Linux (Debian/Ubuntu — adjust for your distro)
|
|
95
|
-
sudo apt install libsecret-1-dev gnome-keyring
|
|
96
|
-
# Windows — install windows-build-tools or VS Build Tools
|
|
97
|
-
|
|
98
|
-
npm install -g @alter-ai/cli
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## Workspace config (`alter link`)
|
|
102
|
-
|
|
103
|
-
If you work primarily on one app, run `alter link <app-id>` once in the project root. The CLI writes a tiny `.alter/config.yaml` that pins the default app for every subsequent `alter keys`, `alter agents`, `alter providers`, and `alter policy show-app` invocation in that directory tree. No more retyping the UUID.
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
cd ~/code/my-product
|
|
107
|
-
alter link app_abc123
|
|
108
|
-
# alter: pinned app_id=app_abc123 in /Users/me/code/my-product/.alter/config.yaml
|
|
109
|
-
# alter: appended `.alter/` to .gitignore so the pin isn't committed.
|
|
110
|
-
|
|
111
|
-
# from anywhere in this tree, --app becomes optional:
|
|
112
|
-
alter keys list
|
|
113
|
-
alter agents create --name worker --type service
|
|
114
|
-
alter policy show-app
|
|
115
|
-
|
|
116
|
-
# show the current pin
|
|
117
|
-
alter link --status
|
|
118
|
-
|
|
119
|
-
# clear the pin
|
|
120
|
-
alter unlink
|
|
121
23
|
```
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
```bash
|
|
139
|
-
# List apps, keep only id + name
|
|
140
|
-
alter apps list --fields id,name
|
|
141
|
-
# [
|
|
142
|
-
# { "id": "app_abc", "name": "demo" },
|
|
143
|
-
# ...
|
|
144
|
-
# ]
|
|
145
|
-
|
|
146
|
-
# Single object — same projection rule
|
|
147
|
-
alter apps show app_abc --fields id,name,environment
|
|
148
|
-
|
|
149
|
-
# JSONL — one projected object per line
|
|
150
|
-
alter audit list --fields timestamp,action --output=jsonl
|
|
24
|
+
auth login | status | logout
|
|
25
|
+
apps list | create | show | update | archive | unarchive | delete
|
|
26
|
+
keys list | mint | show | rotate | revoke | rename
|
|
27
|
+
agents list | create | show | update | revoke (+ mint-key, list-keys, revoke-key)
|
|
28
|
+
providers list | list-catalog | create | show | update | delete
|
|
29
|
+
managed-secrets list | show | create | rotate | delete | templates | access | users
|
|
30
|
+
grants {list, list-for-agent, create, update, revoke}
|
|
31
|
+
groups {list, show}
|
|
32
|
+
policy show-app
|
|
33
|
+
audit list | show | portal-actions | grant-events | traces
|
|
34
|
+
pats whoami
|
|
35
|
+
link / unlink pin an app to the current directory
|
|
36
|
+
completion install | print
|
|
37
|
+
self-update --to <v>
|
|
38
|
+
sdk-passthrough request <grant-id> --url <url>
|
|
151
39
|
```
|
|
152
40
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
## Exit codes
|
|
156
|
-
|
|
157
|
-
`alter` returns a structured exit code so scripts can branch on the failure mode without parsing stderr text. Codes are stable contract — operators can rely on them across releases.
|
|
158
|
-
|
|
159
|
-
| Code | Name | Meaning |
|
|
160
|
-
|------|------------------|-----------------------------------------------------------------------------------------------|
|
|
161
|
-
| 0 | OK | Command succeeded. |
|
|
162
|
-
| 1 | ERROR | Generic runtime failure (uncategorized — including unknown SDK / network errors). |
|
|
163
|
-
| 2 | USAGE | Bad flag, arg, or input format. Always paired with a stderr line naming the offending input. |
|
|
164
|
-
| 3 | AUTH | Not signed in, or PAT revoked / expired. **Remediation:** re-run `alter auth login`. |
|
|
165
|
-
| 4 | NOT_FOUND | Resource not found — 404 from the backend, or a referenced local file is missing. |
|
|
166
|
-
| 5 | CONFLICT | 409 from the backend — most commonly a type-to-confirm mismatch or dependent-resource block. |
|
|
167
|
-
| 6 | RATE_LIMIT | 429 from the backend — retry with backoff. |
|
|
168
|
-
| 7 | FORBIDDEN | 403 from the backend — PAT is valid but lacks the required scope. **Remediation:** re-mint the PAT with broader scopes (or switch PATs); `alter auth login` alone does NOT help. |
|
|
169
|
-
| 8 | CANCELLED | Operator declined an interactive prompt (type-to-confirm mismatch on a destructive action, "no" at a y/N gate). Distinct from `ERROR` (1) — the CLI did nothing wrong, the operator chose not to proceed. Pass `--yes` or `--confirm <name>` in CI to skip the prompt. |
|
|
170
|
-
|
|
171
|
-
Example:
|
|
172
|
-
|
|
173
|
-
```bash
|
|
174
|
-
# Probe whether an app exists without erroring on the not-found case.
|
|
175
|
-
# Capture $? into a local variable BEFORE running anything else; ``$?``
|
|
176
|
-
# is clobbered by every command, so a stray ``log_attempt`` between the
|
|
177
|
-
# ``if`` and ``elif`` would silently break the not-found branch.
|
|
178
|
-
alter apps show "$APP_ID" --output=json > /dev/null 2>&1
|
|
179
|
-
status=$?
|
|
180
|
-
if [ "$status" -eq 0 ]; then
|
|
181
|
-
echo "app exists"
|
|
182
|
-
elif [ "$status" -eq 4 ]; then
|
|
183
|
-
echo "app not found"
|
|
184
|
-
elif [ "$status" -eq 7 ]; then
|
|
185
|
-
echo "PAT lacks dashboard_apps:read — re-mint with broader scopes"
|
|
186
|
-
else
|
|
187
|
-
echo "unexpected error" && exit 1
|
|
188
|
-
fi
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
Backend-thrown errors flow through `withClient` and get mapped from HTTP status to exit code automatically (401 → 3, 403 → 7, 404 → 4, 409 → 5, 429 → 6, everything else → 1). Validation errors raised by the CLI itself (e.g. malformed `--limit`) exit 2.
|
|
192
|
-
|
|
193
|
-
## Scope
|
|
194
|
-
|
|
195
|
-
This CLI ships the full dev-portal command surface — `auth`, `apps`, `keys`, `agents`, `providers`, `managed-secrets`, `policy` (read-only at the app level), `audit`, `pats`, `link` / `unlink`, `completion`, `self-update`, and `sdk-passthrough` as a typed-route escape hatch. Backend routes are PAT-callable via `dashboard_*` scopes (see the [scope catalog](https://docs.alterauth.com/api-reference/scopes) for the full list).
|
|
196
|
-
|
|
197
|
-
**Managed secrets — destructive verb tier:** `alter managed-secrets delete <secret-id>` cascade-revokes every grant and delegation tied to the secret, removes the stored credential from secret storage, and writes cascade audit log entries — irrecoverable. The route is gated by `dashboard_secrets:delete` (NOT bundled into `:write` or `:admin`) AND requires `?confirm=<slug>` matching the target secret's slug. The CLI prompts interactively when stdin is a TTY; CI must pass `--confirm <slug>` explicitly. Mirrors `alter apps delete`. Soft-delete operations on grants (`grants revoke`) use the recoverable `:write` tier.
|
|
198
|
-
|
|
199
|
-
**Managed secrets — credential intake:** `create` and `rotate` accept the credential value through three channels, in decreasing safety order: `--credential-value -` reads one line from stdin (preferred for CI piping), `--credential-value @/path/to/file` reads from a file, and `--credential-value <value>` accepts the value inline with a stderr warning about shell-history leakage. Multi-field templates (those whose backend Pydantic model requires more than a single primary credential string) take `--credentials @file.json` (a JSON object of string fields, takes precedence) or repeated `--credential-field key=value` flags.
|
|
200
|
-
|
|
201
|
-
**Dashboard-only operations** (intentional, not scope gaps):
|
|
41
|
+
All commands accept `--output=json|table|jsonl` and `--fields a,b,c`. See [scripting](https://docs.alterauth.com/reference/cli/scripting) for exit codes and CI patterns, and [authentication](https://docs.alterauth.com/reference/cli/authentication) for token storage details.
|
|
202
42
|
|
|
203
|
-
|
|
204
|
-
- Org-wide key policy (`/organizations/current/key-policy`) — reading the response body is a security-posture fingerprint, so the route refuses PAT auth on both reads and writes.
|
|
205
|
-
- Identity-provider configuration — affects every grant in the org; never settable by a single scripted call.
|
|
206
|
-
- App-level policy *writes* / *deletes* — only `policy show-app` is exposed by the CLI; mutations remain dashboard-only until a CLI use case emerges.
|
|
43
|
+
## License
|
|
207
44
|
|
|
208
|
-
|
|
45
|
+
MIT
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/cli.ts
|
|
4
|
-
import { Command as Command14 } from "commander";
|
|
5
|
-
|
|
6
3
|
// src/exit-codes.ts
|
|
7
4
|
var EXIT_ERROR = 1;
|
|
8
5
|
var EXIT_USAGE = 2;
|
|
@@ -176,7 +173,7 @@ async function maybePrintUpdateBanner(currentVersion, argv2 = process.argv.slice
|
|
|
176
173
|
// package.json
|
|
177
174
|
var package_default = {
|
|
178
175
|
name: "@alter-ai/cli",
|
|
179
|
-
version: "0.1
|
|
176
|
+
version: "0.3.1",
|
|
180
177
|
description: "Command-line interface for the Alter Vault dev portal \u2014 scripted dashboard automation.",
|
|
181
178
|
type: "module",
|
|
182
179
|
bin: {
|
|
@@ -192,7 +189,9 @@ var package_default = {
|
|
|
192
189
|
dev: "tsx src/cli.ts",
|
|
193
190
|
test: "vitest run",
|
|
194
191
|
"test:watch": "vitest",
|
|
195
|
-
|
|
192
|
+
"docs:check": "tsx scripts/check-docs-drift.ts",
|
|
193
|
+
"docs:print": "tsx scripts/check-docs-drift.ts --print",
|
|
194
|
+
typecheck: "tsc --noEmit && tsc -p tsconfig.scripts.json",
|
|
196
195
|
lint: "eslint src/ tests/ --ext .ts",
|
|
197
196
|
format: "prettier --write 'src/**/*.ts' 'tests/**/*.ts'"
|
|
198
197
|
},
|
|
@@ -224,6 +223,9 @@ var package_default = {
|
|
|
224
223
|
}
|
|
225
224
|
};
|
|
226
225
|
|
|
226
|
+
// src/program.ts
|
|
227
|
+
import { Command as Command14 } from "commander";
|
|
228
|
+
|
|
227
229
|
// src/commands/agents.ts
|
|
228
230
|
import { Command } from "commander";
|
|
229
231
|
|
|
@@ -440,10 +442,23 @@ var GrantRevokedError = class extends ReAuthRequiredError {
|
|
|
440
442
|
};
|
|
441
443
|
var CredentialRevokedError = class extends ReAuthRequiredError {
|
|
442
444
|
grantId;
|
|
443
|
-
|
|
445
|
+
// Recovery-context fields. Populated by the backend at the raise
|
|
446
|
+
// site in token_service so callers can mint a Connect session for
|
|
447
|
+
// the same (user, provider) without re-fetching the grant. See
|
|
448
|
+
// docs/planning/CONNECT_RECOVERY_CONTEXT.md.
|
|
449
|
+
providerId;
|
|
450
|
+
appUserId;
|
|
451
|
+
// Recovery context is appended AFTER `details` so the existing
|
|
452
|
+
// positional contract `new CredentialRevokedError("msg", "g-1",
|
|
453
|
+
// detailsObj)` keeps compiling AND keeps assigning detailsObj to
|
|
454
|
+
// the `details` slot — only the wire-parsing site uses the new
|
|
455
|
+
// positional slots.
|
|
456
|
+
constructor(message, grantId, details, providerId, appUserId) {
|
|
444
457
|
super(message, details);
|
|
445
458
|
this.name = "CredentialRevokedError";
|
|
446
459
|
this.grantId = grantId;
|
|
460
|
+
this.providerId = providerId;
|
|
461
|
+
this.appUserId = appUserId;
|
|
447
462
|
}
|
|
448
463
|
};
|
|
449
464
|
var GrantDeletedError = class extends ReAuthRequiredError {
|
|
@@ -453,9 +468,19 @@ var GrantDeletedError = class extends ReAuthRequiredError {
|
|
|
453
468
|
}
|
|
454
469
|
};
|
|
455
470
|
var GrantNotFoundError = class extends BackendError {
|
|
456
|
-
|
|
471
|
+
providerId;
|
|
472
|
+
agentId;
|
|
473
|
+
appUserId;
|
|
474
|
+
// Recovery context is appended AFTER `details` so the existing
|
|
475
|
+
// positional contract `new GrantNotFoundError("msg", detailsObj)`
|
|
476
|
+
// keeps compiling AND keeps assigning detailsObj to the `details`
|
|
477
|
+
// slot — only the wire-parsing site uses the new positional slots.
|
|
478
|
+
constructor(message, details, providerId, agentId, appUserId) {
|
|
457
479
|
super(message, details);
|
|
458
480
|
this.name = "GrantNotFoundError";
|
|
481
|
+
this.providerId = providerId;
|
|
482
|
+
this.agentId = agentId;
|
|
483
|
+
this.appUserId = appUserId;
|
|
459
484
|
}
|
|
460
485
|
};
|
|
461
486
|
var AmbiguousGrantError = class extends BackendError {
|
|
@@ -468,23 +493,43 @@ var AmbiguousGrantError = class extends BackendError {
|
|
|
468
493
|
// UUIDs only (no emails / names) — same backend hygiene rule that
|
|
469
494
|
// applies to the rest of the cross-tenant probe surface.
|
|
470
495
|
appUserIds;
|
|
471
|
-
|
|
496
|
+
// Populated for the managed-secret grant-level flavor: one user
|
|
497
|
+
// delegated multiple managed-secret grants sharing the same
|
|
498
|
+
// template to the same agent. The SDK caller picks one via
|
|
499
|
+
// `grantId=` on the next request. UUIDs only.
|
|
500
|
+
grantIds;
|
|
501
|
+
constructor(message, providerId, accountIdentifiers, accountWasProvided, appUserIds, details, grantIds) {
|
|
472
502
|
super(message, details);
|
|
473
503
|
this.name = "AmbiguousGrantError";
|
|
474
504
|
this.providerId = providerId;
|
|
475
505
|
this.accountIdentifiers = accountIdentifiers ?? [];
|
|
476
506
|
this.accountWasProvided = accountWasProvided ?? false;
|
|
477
507
|
this.appUserIds = appUserIds ?? [];
|
|
508
|
+
this.grantIds = grantIds ?? [];
|
|
478
509
|
}
|
|
479
510
|
};
|
|
480
511
|
var NoDelegatedGrantError = class extends BackendError {
|
|
512
|
+
// `providerId` + `agentId` shipped in the pre-recovery PR, so they
|
|
513
|
+
// KEEP their positional slots. `appUserId` is new in this PR and
|
|
514
|
+
// appended AFTER `details` so the existing 4-arg call shape
|
|
515
|
+
// `new NoDelegatedGrantError(msg, providerId, agentId, details)`
|
|
516
|
+
// continues to work — only the wire-parsing site uses the new
|
|
517
|
+
// appUserId slot.
|
|
481
518
|
providerId;
|
|
482
519
|
agentId;
|
|
483
|
-
|
|
520
|
+
// Populated when the resolution applied a user filter. Diagnostic /
|
|
521
|
+
// correlation field only — the recovery helper does NOT auto-bind
|
|
522
|
+
// the session to the user. Pass `userToken=` to
|
|
523
|
+
// `createConnectSessionForError` (or configure `userTokenGetter` on
|
|
524
|
+
// the client) when user binding is required. See
|
|
525
|
+
// docs/planning/CONNECT_RECOVERY_CONTEXT.md.
|
|
526
|
+
appUserId;
|
|
527
|
+
constructor(message, providerId, agentId, details, appUserId) {
|
|
484
528
|
super(message, details);
|
|
485
529
|
this.name = "NoDelegatedGrantError";
|
|
486
530
|
this.providerId = providerId;
|
|
487
531
|
this.agentId = agentId;
|
|
532
|
+
this.appUserId = appUserId;
|
|
488
533
|
}
|
|
489
534
|
};
|
|
490
535
|
var PolicyViolationError = class extends BackendError {
|
|
@@ -2895,7 +2940,7 @@ function _extractAdditionalCredentials(token) {
|
|
|
2895
2940
|
return _additionalCredsStore.get(token);
|
|
2896
2941
|
}
|
|
2897
2942
|
var _fetch;
|
|
2898
|
-
var SDK_VERSION = "0.
|
|
2943
|
+
var SDK_VERSION = "0.15.0";
|
|
2899
2944
|
var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
|
|
2900
2945
|
var HTTP_FORBIDDEN = 403;
|
|
2901
2946
|
var HTTP_NO_CONTENT2 = 204;
|
|
@@ -3506,7 +3551,10 @@ ${effectiveConstraints}`;
|
|
|
3506
3551
|
if (errorCode === "credential_revoked") {
|
|
3507
3552
|
throw new CredentialRevokedError(
|
|
3508
3553
|
errorData.message ?? "Credential has been revoked. User must re-authorize.",
|
|
3509
|
-
errorData.grant_id
|
|
3554
|
+
errorData.grant_id,
|
|
3555
|
+
errorData,
|
|
3556
|
+
typeof errorData.provider_id === "string" ? errorData.provider_id : void 0,
|
|
3557
|
+
typeof errorData.app_user_id === "string" ? errorData.app_user_id : void 0
|
|
3510
3558
|
);
|
|
3511
3559
|
}
|
|
3512
3560
|
if (errorData.error === "scope_mismatch") {
|
|
@@ -3546,13 +3594,17 @@ ${effectiveConstraints}`;
|
|
|
3546
3594
|
const appUserIds = Array.isArray(errorData.app_user_ids) ? errorData.app_user_ids.filter(
|
|
3547
3595
|
(v) => typeof v === "string"
|
|
3548
3596
|
) : [];
|
|
3597
|
+
const grantIds = Array.isArray(errorData.grant_ids) ? errorData.grant_ids.filter(
|
|
3598
|
+
(v) => typeof v === "string"
|
|
3599
|
+
) : [];
|
|
3549
3600
|
throw new AmbiguousGrantError(
|
|
3550
3601
|
errorData.message ?? "JWT identity resolved to multiple grants. Pass the 'account' parameter to disambiguate.",
|
|
3551
3602
|
typeof errorData.provider_id === "string" ? errorData.provider_id : void 0,
|
|
3552
3603
|
accountIdentifiers,
|
|
3553
3604
|
errorData.account_was_provided === true,
|
|
3554
3605
|
appUserIds,
|
|
3555
|
-
errorData
|
|
3606
|
+
errorData,
|
|
3607
|
+
grantIds
|
|
3556
3608
|
);
|
|
3557
3609
|
}
|
|
3558
3610
|
if (errorData.error === "no_delegated_grant") {
|
|
@@ -3560,7 +3612,8 @@ ${effectiveConstraints}`;
|
|
|
3560
3612
|
errorData.message ?? "Agent has no active delegation or managed-secret grant for this provider.",
|
|
3561
3613
|
typeof errorData.provider_id === "string" ? errorData.provider_id : void 0,
|
|
3562
3614
|
typeof errorData.agent_id === "string" ? errorData.agent_id : void 0,
|
|
3563
|
-
errorData
|
|
3615
|
+
errorData,
|
|
3616
|
+
typeof errorData.app_user_id === "string" ? errorData.app_user_id : void 0
|
|
3564
3617
|
);
|
|
3565
3618
|
}
|
|
3566
3619
|
throw new BackendError(
|
|
@@ -3577,9 +3630,21 @@ ${effectiveConstraints}`;
|
|
|
3577
3630
|
}
|
|
3578
3631
|
if (response.status === HTTP_NOT_FOUND) {
|
|
3579
3632
|
const errorData = await __VaultClient.#safeParseJson(response);
|
|
3633
|
+
if (errorData.error === "no_delegated_grant") {
|
|
3634
|
+
throw new NoDelegatedGrantError(
|
|
3635
|
+
errorData.message ?? "Agent has no active delegation or managed-secret grant for this provider.",
|
|
3636
|
+
typeof errorData.provider_id === "string" ? errorData.provider_id : void 0,
|
|
3637
|
+
typeof errorData.agent_id === "string" ? errorData.agent_id : void 0,
|
|
3638
|
+
errorData,
|
|
3639
|
+
typeof errorData.app_user_id === "string" ? errorData.app_user_id : void 0
|
|
3640
|
+
);
|
|
3641
|
+
}
|
|
3580
3642
|
throw new GrantNotFoundError(
|
|
3581
3643
|
errorData.message ?? "OAuth grant not found for the given grant_id",
|
|
3582
|
-
errorData
|
|
3644
|
+
errorData,
|
|
3645
|
+
typeof errorData.provider_id === "string" ? errorData.provider_id : void 0,
|
|
3646
|
+
typeof errorData.agent_id === "string" ? errorData.agent_id : void 0,
|
|
3647
|
+
typeof errorData.app_user_id === "string" ? errorData.app_user_id : void 0
|
|
3583
3648
|
);
|
|
3584
3649
|
}
|
|
3585
3650
|
if (response.status === HTTP_BAD_REQUEST || response.status === HTTP_BAD_GATEWAY) {
|
|
@@ -3595,7 +3660,9 @@ ${effectiveConstraints}`;
|
|
|
3595
3660
|
throw new CredentialRevokedError(
|
|
3596
3661
|
errorData.message ?? "Underlying credential has been revoked. User must re-authorize.",
|
|
3597
3662
|
errorData.grant_id,
|
|
3598
|
-
errorData
|
|
3663
|
+
errorData,
|
|
3664
|
+
typeof errorData.provider_id === "string" ? errorData.provider_id : void 0,
|
|
3665
|
+
typeof errorData.app_user_id === "string" ? errorData.app_user_id : void 0
|
|
3599
3666
|
);
|
|
3600
3667
|
}
|
|
3601
3668
|
throw new BackendError(
|
|
@@ -4787,7 +4854,14 @@ ${effectiveConstraints}`;
|
|
|
4787
4854
|
}
|
|
4788
4855
|
sessionBody.agent = options.agent;
|
|
4789
4856
|
}
|
|
4790
|
-
if (
|
|
4857
|
+
if (options.userToken !== void 0) {
|
|
4858
|
+
if (typeof options.userToken !== "string" || options.userToken.length === 0) {
|
|
4859
|
+
throw new AlterValueError(
|
|
4860
|
+
"userToken must be a non-empty string when provided"
|
|
4861
|
+
);
|
|
4862
|
+
}
|
|
4863
|
+
sessionBody.user_token = options.userToken;
|
|
4864
|
+
} else if (this.#userTokenGetter) {
|
|
4791
4865
|
sessionBody.user_token = await this.#getUserToken();
|
|
4792
4866
|
}
|
|
4793
4867
|
const sessionPath = "/sdk/oauth/connect/session";
|
|
@@ -4951,10 +5025,43 @@ ${effectiveConstraints}`;
|
|
|
4951
5025
|
} else {
|
|
4952
5026
|
console.log(`Open this URL to authorize: ${session.connectUrl}`);
|
|
4953
5027
|
}
|
|
5028
|
+
return await this.pollConnectSession(session.sessionToken, {
|
|
5029
|
+
timeoutMs,
|
|
5030
|
+
pollIntervalMs
|
|
5031
|
+
});
|
|
5032
|
+
}
|
|
5033
|
+
/**
|
|
5034
|
+
* Poll a Connect session to completion.
|
|
5035
|
+
*
|
|
5036
|
+
* Use this when your code minted the Connect session itself (e.g.,
|
|
5037
|
+
* via {@link createConnectSession} for a custom UI flow, or via
|
|
5038
|
+
* {@link createConnectSessionForError} for a recovery flow) and
|
|
5039
|
+
* you need to block until the user finishes the consent screen.
|
|
5040
|
+
* {@link connect} is the all-in-one convenience that mints + opens
|
|
5041
|
+
* the browser + polls; this method is the polling half on its own.
|
|
5042
|
+
*
|
|
5043
|
+
* @param sessionToken - A session token from
|
|
5044
|
+
* {@link createConnectSession} or
|
|
5045
|
+
* {@link createConnectSessionForError}.
|
|
5046
|
+
* @param options - `timeoutMs` (default 300000 = 5 min) and
|
|
5047
|
+
* `pollIntervalMs` (default 2000). Milliseconds per ecosystem
|
|
5048
|
+
* convention; the Python SDK's equivalent uses seconds.
|
|
5049
|
+
* @returns One `ConnectResult` per provider the user completed
|
|
5050
|
+
* within the session (multi-provider Connect sessions yield
|
|
5051
|
+
* multiple results).
|
|
5052
|
+
* @throws ConnectTimeoutError if the session didn't complete within
|
|
5053
|
+
* `timeoutMs`.
|
|
5054
|
+
* @throws ConnectFlowError / ConnectDeniedError / ConnectConfigError
|
|
5055
|
+
* for user denial, session expiry, or unrecognized status.
|
|
5056
|
+
* @throws AlterSDKError if the SDK instance has been closed.
|
|
5057
|
+
*/
|
|
5058
|
+
async pollConnectSession(sessionToken, options) {
|
|
5059
|
+
this.#assertNotClosed();
|
|
5060
|
+
const timeoutMs = options?.timeoutMs ?? 3e5;
|
|
5061
|
+
const pollIntervalMs = options?.pollIntervalMs ?? 2e3;
|
|
4954
5062
|
const deadline = Date.now() + timeoutMs;
|
|
4955
|
-
while (
|
|
4956
|
-
|
|
4957
|
-
const pollResult = await this.#pollSession(session.sessionToken);
|
|
5063
|
+
while (true) {
|
|
5064
|
+
const pollResult = await this.#pollSession(sessionToken);
|
|
4958
5065
|
const pollStatus = pollResult.status;
|
|
4959
5066
|
if (pollStatus === "completed") {
|
|
4960
5067
|
const grantsData = pollResult.grants ?? [];
|
|
@@ -4997,12 +5104,95 @@ ${effectiveConstraints}`;
|
|
|
4997
5104
|
{ status: pollStatus }
|
|
4998
5105
|
);
|
|
4999
5106
|
}
|
|
5107
|
+
const remaining = deadline - Date.now();
|
|
5108
|
+
if (remaining <= 0) break;
|
|
5109
|
+
const sleepMs = Math.min(pollIntervalMs, remaining);
|
|
5110
|
+
await new Promise((resolve2) => setTimeout(resolve2, sleepMs));
|
|
5000
5111
|
}
|
|
5001
5112
|
throw new ConnectTimeoutError(
|
|
5002
5113
|
`OAuth flow did not complete within ${Math.round(timeoutMs / 1e3)} seconds. The user may not have finished authorizing in the browser.`,
|
|
5003
5114
|
{ timeoutMs }
|
|
5004
5115
|
);
|
|
5005
5116
|
}
|
|
5117
|
+
/**
|
|
5118
|
+
* Mint a recovery Connect session from a typed error.
|
|
5119
|
+
*
|
|
5120
|
+
* Use this in the catch block of an identity-mode or agent-mode
|
|
5121
|
+
* request that failed because the user hasn't authorized the
|
|
5122
|
+
* provider yet ({@link NoDelegatedGrantError}), or because the
|
|
5123
|
+
* underlying credential is permanently broken
|
|
5124
|
+
* ({@link CredentialRevokedError}), or because the resolved grant
|
|
5125
|
+
* doesn't exist ({@link GrantNotFoundError} with identity-mode
|
|
5126
|
+
* context).
|
|
5127
|
+
*
|
|
5128
|
+
* What the method reuses from the error:
|
|
5129
|
+
* - `providerId` → threaded into `allowedProviders=[...]`. Required;
|
|
5130
|
+
* missing context throws `AlterValueError`.
|
|
5131
|
+
* - `agentId` → threaded into `agent=` so the recovery session
|
|
5132
|
+
* re-binds the same delegation target. Required for
|
|
5133
|
+
* `NoDelegatedGrantError` recovery; absent on
|
|
5134
|
+
* `GrantNotFoundError` / `CredentialRevokedError` for user-direct
|
|
5135
|
+
* grants, in which case the call falls through to non-delegated
|
|
5136
|
+
* recovery.
|
|
5137
|
+
*
|
|
5138
|
+
* What the method does NOT reuse from the error:
|
|
5139
|
+
* - `appUserId` is exposed on the typed error for caller use
|
|
5140
|
+
* (logging, audit correlation, deciding which user to re-prompt)
|
|
5141
|
+
* but is NOT threaded into the recovery session.
|
|
5142
|
+
* `createConnectSession` binds the session via `userToken` (JWT),
|
|
5143
|
+
* not `appUserId`. To bind the session, pass `userToken=`
|
|
5144
|
+
* explicitly or configure `userTokenGetter` on the SDK client.
|
|
5145
|
+
*
|
|
5146
|
+
* @example
|
|
5147
|
+
* ```ts
|
|
5148
|
+
* try {
|
|
5149
|
+
* await vault.request(HttpMethod.GET, "https://...", { provider: "<provider-id>" });
|
|
5150
|
+
* } catch (e) {
|
|
5151
|
+
* if (e instanceof NoDelegatedGrantError) {
|
|
5152
|
+
* const session = await vault.createConnectSessionForError(e, {
|
|
5153
|
+
* allowedOrigin: "https://app.example.com",
|
|
5154
|
+
* });
|
|
5155
|
+
* redirectUser(session.connectUrl);
|
|
5156
|
+
* const results = await vault.pollConnectSession(session.sessionToken);
|
|
5157
|
+
* // Retry with results[0].grantId
|
|
5158
|
+
* }
|
|
5159
|
+
* }
|
|
5160
|
+
* ```
|
|
5161
|
+
*
|
|
5162
|
+
* @param error - The typed exception. Must carry `providerId`; if
|
|
5163
|
+
* `undefined` (direct-mode 404 from a stale grant_id), recovery
|
|
5164
|
+
* isn't derivable and this method throws `AlterValueError`
|
|
5165
|
+
* rather than guessing.
|
|
5166
|
+
* @param options - Standard `createConnectSession` options. Supplied
|
|
5167
|
+
* per call so the recovery session matches the deployment
|
|
5168
|
+
* shape (popup, mobile redirect, headless). The convenience
|
|
5169
|
+
* method doesn't infer these from the error.
|
|
5170
|
+
* @throws AlterValueError if `error.providerId` is `undefined`.
|
|
5171
|
+
*/
|
|
5172
|
+
async createConnectSessionForError(error, options) {
|
|
5173
|
+
const providerId = error.providerId;
|
|
5174
|
+
if (providerId === void 0) {
|
|
5175
|
+
throw new AlterValueError(
|
|
5176
|
+
"Cannot mint a recovery Connect session: the typed error has no providerId context. This usually means the original call used direct grantId mode and the grantId was stale \u2014 there's no (user, provider) tuple to recover. Catch GrantNotFoundError separately and call createConnectSession() directly with the providers the user should re-authorize."
|
|
5177
|
+
);
|
|
5178
|
+
}
|
|
5179
|
+
const agentId = error.agentId;
|
|
5180
|
+
if (error instanceof NoDelegatedGrantError && agentId === void 0) {
|
|
5181
|
+
throw new AlterValueError(
|
|
5182
|
+
"Cannot mint a delegated recovery Connect session: the typed NoDelegatedGrantError has no agentId context. The backend may be on a version predating the recovery-context fields, or the wire payload was malformed. Upgrade the backend, or catch this and call createConnectSession() explicitly with the right agent=."
|
|
5183
|
+
);
|
|
5184
|
+
}
|
|
5185
|
+
return await this.createConnectSession({
|
|
5186
|
+
allowedProviders: [providerId],
|
|
5187
|
+
allowedOrigin: options?.allowedOrigin,
|
|
5188
|
+
returnUrl: options?.returnUrl,
|
|
5189
|
+
metadata: options?.metadata,
|
|
5190
|
+
grantPolicy: options?.grantPolicy,
|
|
5191
|
+
requiredScopes: options?.requiredScopes,
|
|
5192
|
+
agent: agentId,
|
|
5193
|
+
userToken: options?.userToken
|
|
5194
|
+
});
|
|
5195
|
+
}
|
|
5006
5196
|
/**
|
|
5007
5197
|
* Trigger IDP login for end user via browser.
|
|
5008
5198
|
*
|
|
@@ -6539,8 +6729,13 @@ var _keytarCache;
|
|
|
6539
6729
|
async function loadKeytar() {
|
|
6540
6730
|
if (_keytarCache !== void 0) return _keytarCache;
|
|
6541
6731
|
try {
|
|
6542
|
-
const
|
|
6543
|
-
|
|
6732
|
+
const raw = await import("keytar");
|
|
6733
|
+
const candidate = raw.default ?? raw;
|
|
6734
|
+
if (typeof candidate.getPassword === "function" && typeof candidate.setPassword === "function" && typeof candidate.deletePassword === "function") {
|
|
6735
|
+
_keytarCache = candidate;
|
|
6736
|
+
} else {
|
|
6737
|
+
_keytarCache = null;
|
|
6738
|
+
}
|
|
6544
6739
|
} catch {
|
|
6545
6740
|
_keytarCache = null;
|
|
6546
6741
|
}
|
|
@@ -6548,7 +6743,7 @@ async function loadKeytar() {
|
|
|
6548
6743
|
}
|
|
6549
6744
|
function printPlaintextFallbackWarning() {
|
|
6550
6745
|
process.stderr.write(
|
|
6551
|
-
"alter: WARNING \u2014 saving the PAT to a plaintext file (~/.config/alter/auth.toml).\n The OS keychain (keytar) is not
|
|
6746
|
+
"alter: WARNING \u2014 saving the PAT to a plaintext file (~/.config/alter/auth.toml, mode 0600).\n The OS keychain (keytar) is not usable on this host. Common causes:\n - keytar's native module failed to install (Linux: install libsecret-1-dev +\n gnome-keyring; Windows: install VS Build Tools; macOS: usually pre-installed).\n - The native module is present but its API surface doesn't match what the CLI\n expects (file a bug against @alter-ai/cli with your node + npm versions).\n If the cleartext fallback is unacceptable on this host, revoke the PAT from the\n dashboard's `Personal Access Tokens` page after use.\n"
|
|
6552
6747
|
);
|
|
6553
6748
|
}
|
|
6554
6749
|
async function loadStoredAuth2() {
|
|
@@ -6642,11 +6837,11 @@ async function clearStoredAuth2() {
|
|
|
6642
6837
|
|
|
6643
6838
|
// src/portal-client.ts
|
|
6644
6839
|
import { platform, release } from "os";
|
|
6645
|
-
var DEFAULT_BASE_URL = "https://
|
|
6840
|
+
var DEFAULT_BASE_URL = "https://backend.alterauth.com";
|
|
6646
6841
|
var PAT_API_PREFIX = "/api/v1/dev-portal";
|
|
6647
6842
|
var HTTP_ERROR_THRESHOLD = 400;
|
|
6648
6843
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
6649
|
-
var CLI_VERSION = "0.1
|
|
6844
|
+
var CLI_VERSION = "0.3.1";
|
|
6650
6845
|
var USER_AGENT = buildUserAgent();
|
|
6651
6846
|
function buildUserAgent() {
|
|
6652
6847
|
let osTag = "";
|
|
@@ -7577,6 +7772,31 @@ var ManagedSecretsNamespace = class {
|
|
|
7577
7772
|
);
|
|
7578
7773
|
return expectDict(body, "managed_secrets.rotate", 200);
|
|
7579
7774
|
}
|
|
7775
|
+
/**
|
|
7776
|
+
* Set the per-secret user → agent delegation policy. Requires
|
|
7777
|
+
* ``dashboard_secrets:write``. Controls whether a group-typed grant on
|
|
7778
|
+
* this secret may be delegated to an agent (``allow_group_source``) and
|
|
7779
|
+
* the per-delegation TTL ceiling (``max_delegation_ttl_days``).
|
|
7780
|
+
*
|
|
7781
|
+
* REPLACE semantics — the backend overwrites the whole policy object,
|
|
7782
|
+
* so the command always sends the full intended state (omitting
|
|
7783
|
+
* ``max_delegation_ttl_days`` lets the backend apply its 90-day default).
|
|
7784
|
+
*/
|
|
7785
|
+
async setDelegationPolicy(appId, secretId, options) {
|
|
7786
|
+
const app = encodePathParam(appId, "appId");
|
|
7787
|
+
const secret = encodePathParam(secretId, "secretId");
|
|
7788
|
+
const payload = filterUndefined({
|
|
7789
|
+
allow_group_source: options.allow_group_source,
|
|
7790
|
+
max_delegation_ttl_days: options.max_delegation_ttl_days
|
|
7791
|
+
});
|
|
7792
|
+
const body = await this.#client._call(
|
|
7793
|
+
"PUT",
|
|
7794
|
+
`/apps/${app}/managed-secrets/${secret}/delegation-policy`,
|
|
7795
|
+
"managed_secrets.set_delegation_policy",
|
|
7796
|
+
{ jsonBody: payload }
|
|
7797
|
+
);
|
|
7798
|
+
return expectDict(body, "managed_secrets.set_delegation_policy", 200);
|
|
7799
|
+
}
|
|
7580
7800
|
/**
|
|
7581
7801
|
* List grants on a managed secret. Requires ``dashboard_secrets:read``.
|
|
7582
7802
|
*
|
|
@@ -9383,7 +9603,7 @@ var DEFAULT_SCOPES = [
|
|
|
9383
9603
|
"dashboard_secrets:write"
|
|
9384
9604
|
];
|
|
9385
9605
|
function deriveDashboardUrl(baseUrl) {
|
|
9386
|
-
return baseUrl.replace(/^https:\/\/
|
|
9606
|
+
return baseUrl.replace(/^https:\/\/backend\./, "https://portal.");
|
|
9387
9607
|
}
|
|
9388
9608
|
function defaultOpenBrowser(url) {
|
|
9389
9609
|
let command;
|
|
@@ -9500,7 +9720,7 @@ async function runBrowserDance(options) {
|
|
|
9500
9720
|
server.close();
|
|
9501
9721
|
reject(
|
|
9502
9722
|
new Error(
|
|
9503
|
-
|
|
9723
|
+
`browser-dance callback came from an unexpected origin \u2014 refusing to accept. Expected origin ${JSON.stringify(dashboardOrigin)}; received Origin: ${JSON.stringify(origin || "(none)")}, Referer: ${JSON.stringify(referer || "(none)")}. If your dashboard isn't at the regex-derived host, re-run with --dashboard-url <https-url> pointing at the actual dashboard origin.`
|
|
9504
9724
|
)
|
|
9505
9725
|
);
|
|
9506
9726
|
return;
|
|
@@ -9634,8 +9854,21 @@ async function runBrowserDance(options) {
|
|
|
9634
9854
|
pat,
|
|
9635
9855
|
...options.baseUrl ? { baseUrl: options.baseUrl } : {}
|
|
9636
9856
|
});
|
|
9857
|
+
const smokeTest = options.smokeTest ?? defaultSmokeTest;
|
|
9858
|
+
await smokeTest(pat, options.baseUrl);
|
|
9637
9859
|
process.stdout.write("alter: signed in via browser-dance flow.\n");
|
|
9638
9860
|
}
|
|
9861
|
+
async function defaultSmokeTest(pat, baseUrl) {
|
|
9862
|
+
const client = new DashboardClient({
|
|
9863
|
+
pat,
|
|
9864
|
+
...baseUrl ? { baseUrl } : {}
|
|
9865
|
+
});
|
|
9866
|
+
try {
|
|
9867
|
+
await client.pats.whoami();
|
|
9868
|
+
} finally {
|
|
9869
|
+
await client.close();
|
|
9870
|
+
}
|
|
9871
|
+
}
|
|
9639
9872
|
|
|
9640
9873
|
// src/scope-catalog.ts
|
|
9641
9874
|
var DASHBOARD_RESOURCE_VERBS = {
|
|
@@ -9849,7 +10082,7 @@ async function loginCommand(options) {
|
|
|
9849
10082
|
let resolvedBaseUrl2;
|
|
9850
10083
|
try {
|
|
9851
10084
|
const envBaseUrl = process.env.ALTER_BASE_URL?.trim() || null;
|
|
9852
|
-
resolvedBaseUrl2 = options.baseUrl ? validateBaseUrl(options.baseUrl) : envBaseUrl ? validateBaseUrl(envBaseUrl) : "https://
|
|
10085
|
+
resolvedBaseUrl2 = options.baseUrl ? validateBaseUrl(options.baseUrl) : envBaseUrl ? validateBaseUrl(envBaseUrl) : "https://backend.alterauth.com";
|
|
9853
10086
|
} catch (e) {
|
|
9854
10087
|
err(e instanceof Error ? e.message : String(e));
|
|
9855
10088
|
process.exit(EXIT_USAGE);
|
|
@@ -9875,10 +10108,8 @@ async function loginCommand(options) {
|
|
|
9875
10108
|
process.exit(EXIT_USAGE);
|
|
9876
10109
|
}
|
|
9877
10110
|
}
|
|
9878
|
-
let bindOverride =
|
|
9879
|
-
if (options.bindIp ===
|
|
9880
|
-
bindOverride = { bind: "none" };
|
|
9881
|
-
} else if (typeof options.bindIp === "string") {
|
|
10111
|
+
let bindOverride = { bind: "none" };
|
|
10112
|
+
if (typeof options.bindIp === "string") {
|
|
9882
10113
|
let normalisedCidr;
|
|
9883
10114
|
try {
|
|
9884
10115
|
normalisedCidr = validateBindIpFlag(options.bindIp);
|
|
@@ -9897,6 +10128,19 @@ async function loginCommand(options) {
|
|
|
9897
10128
|
});
|
|
9898
10129
|
return;
|
|
9899
10130
|
} catch (e) {
|
|
10131
|
+
if (e instanceof PortalBackendError) {
|
|
10132
|
+
const isAuthTier = e.statusCode === 401 || e.statusCode === 403;
|
|
10133
|
+
if (isAuthTier) {
|
|
10134
|
+
err(`PAT was minted but cannot authenticate: ${e.message}`);
|
|
10135
|
+
} else {
|
|
10136
|
+
err(`PAT was minted but the backend rejected the smoke test: ${e.message}`);
|
|
10137
|
+
}
|
|
10138
|
+
process.exit(exitCodeForHttpStatus(e.statusCode));
|
|
10139
|
+
}
|
|
10140
|
+
if (e instanceof NetworkError) {
|
|
10141
|
+
err(`PAT was minted but the backend is unreachable: ${e.message}. Check connectivity and retry.`);
|
|
10142
|
+
process.exit(EXIT_ERROR);
|
|
10143
|
+
}
|
|
9900
10144
|
err(e instanceof Error ? e.message : String(e));
|
|
9901
10145
|
process.exit(EXIT_ERROR);
|
|
9902
10146
|
}
|
|
@@ -10061,16 +10305,16 @@ function buildAuthCommand() {
|
|
|
10061
10305
|
"Override the backend URL (must be https://). Defaults to ALTER_BASE_URL or production."
|
|
10062
10306
|
).option(
|
|
10063
10307
|
"--dashboard-url <url>",
|
|
10064
|
-
"Override the dashboard URL used by the browser-dance flow. Defaults to the backend URL with the leftmost `
|
|
10308
|
+
"Override the dashboard URL used by the browser-dance flow. Defaults to the backend URL with the leftmost `backend.` subdomain swapped for `portal.`. Use this when the dashboard isn't at the regex-derivable host (e.g. staging / self-hosted setups). Must use https://."
|
|
10065
10309
|
).option(
|
|
10066
10310
|
"--scopes <list>",
|
|
10067
10311
|
"Comma-separated scope list to request on the browser-dance consent page (e.g. 'dashboard_apps:read,dashboard_keys:write'). Defaults to the full read+write set across the CLI surface \u2014 enough for every non-destructive day-to-day command. Opt in to admin / delete tiers explicitly: 'dashboard_keys:admin' for runtime-key admin ops, 'dashboard_apps:delete' for cascade-delete an app (note: dashboard_apps does NOT support :admin \u2014 the Destructive-Action Policy separates :delete as its own verb). Each entry is validated client-side against the dashboard scope catalog \u2014 typos fail fast. Mutually exclusive with --token / --token-file / --token-stdin \u2014 pre-minted PATs have their scope set baked at mint time on the dashboard's Settings page; combining the flags is a usage error."
|
|
10068
10312
|
).option(
|
|
10069
10313
|
"--no-bind-ip",
|
|
10070
|
-
"
|
|
10314
|
+
"Explicit opt-out of IP binding. This is now the DEFAULT for the browser-dance flow, so the flag is a no-op alias retained for scripts that pass it explicitly. The resulting PAT works from any source IP. Browser-dance flow only."
|
|
10071
10315
|
).option(
|
|
10072
10316
|
"--bind-ip <cidr>",
|
|
10073
|
-
"Mint the PAT bound to the operator-supplied IP / CIDR (e.g. '203.0.113.5' for a single host, '203.0.113.0/24' for a range, or an IPv6 equivalent). The
|
|
10317
|
+
"Mint the PAT bound to the operator-supplied IP / CIDR (e.g. '203.0.113.5' for a single host, '203.0.113.0/24' for a range, or an IPv6 equivalent). The supplied range becomes the entire allowlist on the minted PAT. Use this when the egress IP is known and stable \u2014 e.g. minting a token for a CI runner whose outbound CIDR you control. Interactive developer-laptop logins should leave this unset (default is now unbound; see --no-bind-ip). If both --bind-ip and --no-bind-ip appear, the one that appears LAST on the command line takes effect (Commander last-wins). Browser-dance flow only."
|
|
10074
10318
|
).action(async (options) => {
|
|
10075
10319
|
await loginCommand(options);
|
|
10076
10320
|
});
|
|
@@ -10469,7 +10713,7 @@ function buildKeysCommand() {
|
|
|
10469
10713
|
}
|
|
10470
10714
|
);
|
|
10471
10715
|
keys.command("rotate").description(
|
|
10472
|
-
"Rotate a key (new plaintext returned ONCE; old key enters grace until revoke). Requires dashboard_keys:
|
|
10716
|
+
"Rotate a key (new plaintext returned ONCE; old key enters grace until revoke). Requires dashboard_keys:write scope."
|
|
10473
10717
|
).option("--app <app-id>", "App ID. Falls back to ALTER_APP_ID env or .alter/config.yaml").requiredOption("--key <key-id>", "Key ID", parseUuidArgument("--key")).option(
|
|
10474
10718
|
"--scopes <list>",
|
|
10475
10719
|
"Optional new scope set (defaults to the key's current scopes)"
|
|
@@ -11478,7 +11722,14 @@ function buildManagedSecretsCommand() {
|
|
|
11478
11722
|
).option(
|
|
11479
11723
|
"--injection-rule <@file.json>",
|
|
11480
11724
|
"Path-prefixed JSON file containing the additional_injections array"
|
|
11481
|
-
).option("--label <label>", "Base-grant display label").option("--account-identifier <id>", "Optional account identifier metadata").option("--account-display-name <name>", "Optional account display name metadata").option(
|
|
11725
|
+
).option("--label <label>", "Base-grant display label").option("--account-identifier <id>", "Optional account identifier metadata").option("--account-display-name <name>", "Optional account display name metadata").option(
|
|
11726
|
+
"--allow-group-delegation",
|
|
11727
|
+
"Allow members of a group-typed grant on this secret to delegate it to an agent (default: off)"
|
|
11728
|
+
).option(
|
|
11729
|
+
"--max-delegation-ttl-days <days>",
|
|
11730
|
+
"Cap on a single delegation's lifetime, in days (1..1825, default 90)",
|
|
11731
|
+
parseBoundedInt("--max-delegation-ttl-days", 1, 5 * 365)
|
|
11732
|
+
).option("--output <format>", "Output format: json|table (default: json)", "json").action(
|
|
11482
11733
|
async (options) => {
|
|
11483
11734
|
const format = coerceOutputFormat(options.output);
|
|
11484
11735
|
const appId = resolveAppIdOrExit(options.app);
|
|
@@ -11501,7 +11752,9 @@ function buildManagedSecretsCommand() {
|
|
|
11501
11752
|
"injectionRule",
|
|
11502
11753
|
"label",
|
|
11503
11754
|
"accountIdentifier",
|
|
11504
|
-
"accountDisplayName"
|
|
11755
|
+
"accountDisplayName",
|
|
11756
|
+
"allowGroupDelegation",
|
|
11757
|
+
"maxDelegationTtlDays"
|
|
11505
11758
|
].filter((k) => {
|
|
11506
11759
|
if (k === "credentialType") return options.credentialType !== "bearer_token";
|
|
11507
11760
|
return options[k] !== void 0;
|
|
@@ -11587,6 +11840,16 @@ function buildManagedSecretsCommand() {
|
|
|
11587
11840
|
if (options.accountDisplayName !== void 0) {
|
|
11588
11841
|
body.account_display_name = options.accountDisplayName;
|
|
11589
11842
|
}
|
|
11843
|
+
if (options.allowGroupDelegation !== void 0 || options.maxDelegationTtlDays !== void 0) {
|
|
11844
|
+
const delegationPolicy = {};
|
|
11845
|
+
if (options.allowGroupDelegation !== void 0) {
|
|
11846
|
+
delegationPolicy.allow_group_source = options.allowGroupDelegation;
|
|
11847
|
+
}
|
|
11848
|
+
if (options.maxDelegationTtlDays !== void 0) {
|
|
11849
|
+
delegationPolicy.max_delegation_ttl_days = options.maxDelegationTtlDays;
|
|
11850
|
+
}
|
|
11851
|
+
body.delegation_policy = delegationPolicy;
|
|
11852
|
+
}
|
|
11590
11853
|
await withClient(async (client) => {
|
|
11591
11854
|
const row = await client.managedSecrets.create(
|
|
11592
11855
|
resolveAppIdOrExit(options.app),
|
|
@@ -11596,6 +11859,29 @@ function buildManagedSecretsCommand() {
|
|
|
11596
11859
|
});
|
|
11597
11860
|
}
|
|
11598
11861
|
);
|
|
11862
|
+
root.command("set-delegation-policy <secret-id>").description(
|
|
11863
|
+
"Set the user \u2192 agent delegation policy on a managed secret. REPLACES the current policy (omitting --allow-group-delegation turns group delegation OFF). Requires dashboard_secrets:write."
|
|
11864
|
+
).option("--app <app-id>", "App ID. Falls back to ALTER_APP_ID env or .alter/config.yaml").option(
|
|
11865
|
+
"--allow-group-delegation",
|
|
11866
|
+
"Allow members of a group-typed grant on this secret to delegate it to an agent (default: off)"
|
|
11867
|
+
).option(
|
|
11868
|
+
"--max-delegation-ttl-days <days>",
|
|
11869
|
+
"Cap on a single delegation's lifetime, in days (1..1825; omit for the 90-day default)",
|
|
11870
|
+
parseBoundedInt("--max-delegation-ttl-days", 1, 5 * 365)
|
|
11871
|
+
).option("--output <format>", "Output format: json|table (default: json)", "json").action(
|
|
11872
|
+
async (rawSecretId, options) => {
|
|
11873
|
+
const format = coerceOutputFormat(options.output);
|
|
11874
|
+
const appId = resolveAppIdOrExit(options.app);
|
|
11875
|
+
const secretId = parseUuidArgument("<secret-id>")(rawSecretId);
|
|
11876
|
+
await withClient(async (client) => {
|
|
11877
|
+
const row = await client.managedSecrets.setDelegationPolicy(appId, secretId, {
|
|
11878
|
+
allow_group_source: options.allowGroupDelegation === true,
|
|
11879
|
+
max_delegation_ttl_days: options.maxDelegationTtlDays
|
|
11880
|
+
});
|
|
11881
|
+
emit(format, row);
|
|
11882
|
+
});
|
|
11883
|
+
}
|
|
11884
|
+
);
|
|
11599
11885
|
root.command("delete <secret-id>").description(
|
|
11600
11886
|
"Cascade-delete a managed secret. Requires dashboard_secrets:delete scope (NOT bundled into :write). Cascade-revokes grants + delegations + audit anchors and vault-deletes the credential."
|
|
11601
11887
|
).option("--app <app-id>", "App ID").option(
|
|
@@ -12348,6 +12634,46 @@ function buildSelfUpdateCommand() {
|
|
|
12348
12634
|
});
|
|
12349
12635
|
}
|
|
12350
12636
|
|
|
12637
|
+
// src/program.ts
|
|
12638
|
+
function buildProgram() {
|
|
12639
|
+
const program2 = new Command14();
|
|
12640
|
+
program2.name("alter").description("Alter Vault command-line interface").version(package_default.version);
|
|
12641
|
+
program2.option(
|
|
12642
|
+
"--fields <list>",
|
|
12643
|
+
"Comma-separated top-level keys to keep in JSON output (e.g. ``--fields id,name``). Inert with --output=table."
|
|
12644
|
+
);
|
|
12645
|
+
program2.hook("preAction", (thisCommand) => {
|
|
12646
|
+
setGlobalFields(void 0);
|
|
12647
|
+
const raw = thisCommand.opts().fields;
|
|
12648
|
+
if (raw !== void 0) {
|
|
12649
|
+
try {
|
|
12650
|
+
setGlobalFields(parseFieldsList(raw));
|
|
12651
|
+
} catch (e) {
|
|
12652
|
+
process.stderr.write(
|
|
12653
|
+
`alter: ${e instanceof Error ? e.message : String(e)}
|
|
12654
|
+
`
|
|
12655
|
+
);
|
|
12656
|
+
process.exit(EXIT_USAGE);
|
|
12657
|
+
}
|
|
12658
|
+
}
|
|
12659
|
+
});
|
|
12660
|
+
program2.addCommand(buildAuthCommand());
|
|
12661
|
+
program2.addCommand(buildAppsCommand());
|
|
12662
|
+
program2.addCommand(buildKeysCommand());
|
|
12663
|
+
program2.addCommand(buildAgentsCommand());
|
|
12664
|
+
program2.addCommand(buildProvidersCommand());
|
|
12665
|
+
program2.addCommand(buildManagedSecretsCommand());
|
|
12666
|
+
program2.addCommand(buildPolicyCommand());
|
|
12667
|
+
program2.addCommand(buildAuditCommand());
|
|
12668
|
+
program2.addCommand(buildPatsCommand());
|
|
12669
|
+
program2.addCommand(buildLinkCommand());
|
|
12670
|
+
program2.addCommand(buildUnlinkCommand());
|
|
12671
|
+
program2.addCommand(buildCompletionCommand());
|
|
12672
|
+
program2.addCommand(buildSdkPassthroughCommand());
|
|
12673
|
+
program2.addCommand(buildSelfUpdateCommand());
|
|
12674
|
+
return program2;
|
|
12675
|
+
}
|
|
12676
|
+
|
|
12351
12677
|
// src/cli.ts
|
|
12352
12678
|
var COMMANDER_USAGE_CODES = /* @__PURE__ */ new Set([
|
|
12353
12679
|
"commander.missingArgument",
|
|
@@ -12411,41 +12737,7 @@ function rewriteLegacySelfUpdateVersionFlag(argv2) {
|
|
|
12411
12737
|
}
|
|
12412
12738
|
return rewritten;
|
|
12413
12739
|
}
|
|
12414
|
-
var program =
|
|
12415
|
-
program.name("alter").description("Alter Vault command-line interface").version(package_default.version);
|
|
12416
|
-
program.option(
|
|
12417
|
-
"--fields <list>",
|
|
12418
|
-
"Comma-separated top-level keys to keep in JSON output (e.g. ``--fields id,name``). Inert with --output=table."
|
|
12419
|
-
);
|
|
12420
|
-
program.hook("preAction", (thisCommand) => {
|
|
12421
|
-
setGlobalFields(void 0);
|
|
12422
|
-
const raw = thisCommand.opts().fields;
|
|
12423
|
-
if (raw !== void 0) {
|
|
12424
|
-
try {
|
|
12425
|
-
setGlobalFields(parseFieldsList(raw));
|
|
12426
|
-
} catch (e) {
|
|
12427
|
-
process.stderr.write(
|
|
12428
|
-
`alter: ${e instanceof Error ? e.message : String(e)}
|
|
12429
|
-
`
|
|
12430
|
-
);
|
|
12431
|
-
process.exit(EXIT_USAGE);
|
|
12432
|
-
}
|
|
12433
|
-
}
|
|
12434
|
-
});
|
|
12435
|
-
program.addCommand(buildAuthCommand());
|
|
12436
|
-
program.addCommand(buildAppsCommand());
|
|
12437
|
-
program.addCommand(buildKeysCommand());
|
|
12438
|
-
program.addCommand(buildAgentsCommand());
|
|
12439
|
-
program.addCommand(buildProvidersCommand());
|
|
12440
|
-
program.addCommand(buildManagedSecretsCommand());
|
|
12441
|
-
program.addCommand(buildPolicyCommand());
|
|
12442
|
-
program.addCommand(buildAuditCommand());
|
|
12443
|
-
program.addCommand(buildPatsCommand());
|
|
12444
|
-
program.addCommand(buildLinkCommand());
|
|
12445
|
-
program.addCommand(buildUnlinkCommand());
|
|
12446
|
-
program.addCommand(buildCompletionCommand());
|
|
12447
|
-
program.addCommand(buildSdkPassthroughCommand());
|
|
12448
|
-
program.addCommand(buildSelfUpdateCommand());
|
|
12740
|
+
var program = buildProgram();
|
|
12449
12741
|
applyExitOverride(program);
|
|
12450
12742
|
var argv = rewriteLegacySelfUpdateVersionFlag(process.argv);
|
|
12451
12743
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alter-ai/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Command-line interface for the Alter Vault dev portal — scripted dashboard automation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"dev": "tsx src/cli.ts",
|
|
17
17
|
"test": "vitest run",
|
|
18
18
|
"test:watch": "vitest",
|
|
19
|
-
"
|
|
19
|
+
"docs:check": "tsx scripts/check-docs-drift.ts",
|
|
20
|
+
"docs:print": "tsx scripts/check-docs-drift.ts --print",
|
|
21
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.scripts.json",
|
|
20
22
|
"lint": "eslint src/ tests/ --ext .ts",
|
|
21
23
|
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'"
|
|
22
24
|
},
|