@zalosai/zalos-cli 0.2.13
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/PUBLISHING.md +97 -0
- package/README.md +231 -0
- package/dist/client.js +79 -0
- package/dist/commands/api-token.js +74 -0
- package/dist/commands/auth.js +196 -0
- package/dist/commands/credential.js +15 -0
- package/dist/commands/cycle.js +47 -0
- package/dist/commands/doctor.js +106 -0
- package/dist/commands/execution.js +117 -0
- package/dist/commands/file.js +257 -0
- package/dist/commands/label.js +30 -0
- package/dist/commands/member.js +23 -0
- package/dist/commands/page-markdown.js +213 -0
- package/dist/commands/page.js +178 -0
- package/dist/commands/project.js +97 -0
- package/dist/commands/schema.js +178 -0
- package/dist/commands/setup.js +34 -0
- package/dist/commands/state.js +30 -0
- package/dist/commands/task-comment.js +75 -0
- package/dist/commands/task.js +245 -0
- package/dist/commands/team.js +88 -0
- package/dist/commands/template.js +21 -0
- package/dist/commands/workflow.js +245 -0
- package/dist/credentials.js +123 -0
- package/dist/environments.js +42 -0
- package/dist/main.js +86 -0
- package/dist/utils/cli-auth.js +186 -0
- package/dist/utils/developer-db.js +78 -0
- package/dist/utils/download-file.js +65 -0
- package/dist/utils/errors.js +60 -0
- package/dist/utils/output.js +30 -0
- package/dist/utils/product-api.js +48 -0
- package/dist/utils/stdin.js +23 -0
- package/dist/utils/validate.js +39 -0
- package/dist/workflow-file.js +34 -0
- package/package.json +53 -0
package/PUBLISHING.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Releasing `@zalosai/zalos-cli`
|
|
2
|
+
|
|
3
|
+
Releases are published to the public npm registry by the `Publish Zalos CLI`
|
|
4
|
+
GitHub Actions workflow. The package remains API-only: installing it grants no
|
|
5
|
+
Zalos product access.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- The release commit must be merged into `develop`.
|
|
10
|
+
- The package version must not already exist in npm.
|
|
11
|
+
- The repository `NPM_TOKEN` secret must have publish access to
|
|
12
|
+
`@zalosai/zalos-cli` on npm.
|
|
13
|
+
|
|
14
|
+
## Release
|
|
15
|
+
|
|
16
|
+
1. Bump the version without creating a Git tag:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm --filter @zalosai/zalos-cli exec npm version patch --no-git-tag-version
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use `minor` or `major` instead of `patch` when appropriate.
|
|
23
|
+
|
|
24
|
+
2. Run the package checks at the new version:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm --filter @zalosai/zalos-cli test
|
|
28
|
+
pnpm --filter @zalosai/zalos-cli typecheck
|
|
29
|
+
pnpm --filter @zalosai/zalos-cli build
|
|
30
|
+
pnpm --filter @zalosai/zalos-cli smoke:package
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Commit the version and release changes through the normal pull-request process.
|
|
34
|
+
|
|
35
|
+
3. From the merged release commit, inspect the tarball:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pnpm --filter @zalosai/zalos-cli exec npm pack --dry-run
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Only `package.json`, `README.md`, `PUBLISHING.md`, and runtime JavaScript under `dist/`
|
|
42
|
+
should be included.
|
|
43
|
+
|
|
44
|
+
4. In GitHub, open **Actions → Publish Zalos CLI → Run workflow**.
|
|
45
|
+
Select `develop` and enter the exact version from `package.json`. The workflow rejects
|
|
46
|
+
non-`develop` refs, mismatched versions, and versions that already exist.
|
|
47
|
+
|
|
48
|
+
5. Verify from a clean machine or isolated environment:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install --global @zalosai/zalos-cli@<version>
|
|
52
|
+
zl --version
|
|
53
|
+
zl --help
|
|
54
|
+
zl schema task
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Do not publish with uncommitted package changes or reuse an existing version.
|
|
58
|
+
|
|
59
|
+
## Central developer access release
|
|
60
|
+
|
|
61
|
+
Version 0.2.13 adds revision-safe Markdown import and export for team Pages. It converts common
|
|
62
|
+
Tiptap process-document structures while rejecting unsupported stored nodes during export, and uses
|
|
63
|
+
the `pages:read` and `pages:write` scopes for least-privilege API access.
|
|
64
|
+
Version 0.2.12 exposes immutable team public IDs and accepts them for team switching and trusted
|
|
65
|
+
cross-team Team Files operations while retaining internal UUID compatibility.
|
|
66
|
+
Version 0.2.11 adds authenticated immutable execution-artifact downloads. The command resolves a
|
|
67
|
+
short-lived URL through the execution-artifact API, writes a new private file, and never prints the
|
|
68
|
+
signed URL. Version 0.2.10 lets `file seed` explicitly read prior agent results from the source team's
|
|
69
|
+
`generated-outputs` folder scope while continuing to write normal Team Files in the destination.
|
|
70
|
+
Version 0.2.9 makes Team Files seeding checksum-bound and resumable, and prevents retries from
|
|
71
|
+
reusing object keys that may have a queued deletion. Version 0.2.8 adds preview-first, server-side
|
|
72
|
+
Team Files seeding from an explicit source team into the selected destination team, with collision
|
|
73
|
+
refusal and preserved subtree paths.
|
|
74
|
+
Version 0.2.7 adds browser-authorized, scope- and expiry-bound API token provisioning through
|
|
75
|
+
`zl api-token create`, without database credentials or a database tunnel.
|
|
76
|
+
Version 0.2.6 excludes archived projects from `project list` by default and adds
|
|
77
|
+
`--include-archived` for historical lookup.
|
|
78
|
+
Version 0.2.5 preserves the server's expiry and membership errors for `team switch` while
|
|
79
|
+
other commands retain generic authentication failures. Version 0.2.4 adds authenticated
|
|
80
|
+
team selection without reopening browser login.
|
|
81
|
+
Version 0.2.3 adds authenticated Team Files downloads and removes direct S3 access from the
|
|
82
|
+
operator retrieval path. Version 0.2.2 adds internal cross-team workflow, team, and Team Files
|
|
83
|
+
metadata commands.
|
|
84
|
+
Deploy the corresponding regional API routes before publishing or advertising these commands.
|
|
85
|
+
Version 0.2.1 requires the corresponding scoped-token API deployment and database
|
|
86
|
+
scope-version migration before human login. Keep `@zalos-io/developer-db` as a separately
|
|
87
|
+
versioned private npm package; the Zalos CLI tarball must not bundle its AWS/database
|
|
88
|
+
dependencies. Publish database-tool releases through the `Publish Developer DB` workflow.
|
|
89
|
+
|
|
90
|
+
After merge and publication, use an isolated installation for staging setup/doctor,
|
|
91
|
+
workflow/execution reads, and a dedicated staging workflow mutation. Verify US and EU
|
|
92
|
+
reads independently; do not run production mutation smoke tests. Record the release
|
|
93
|
+
SHA, package version, and regional results before advertising the release.
|
|
94
|
+
|
|
95
|
+
Owner-managed bot rotation and the one-release compatibility observation window are
|
|
96
|
+
required before removing legacy Mesh product commands. Pin the previous immutable npm
|
|
97
|
+
versions for rollback, retaining the Mesh compatibility release.
|
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Zalos CLI
|
|
2
|
+
|
|
3
|
+
`zl` is the globally installed, API-only product CLI. Use it from any repository.
|
|
4
|
+
It emits JSON on stdout, structured errors on stderr, and nonzero exit codes on failure.
|
|
5
|
+
`zalos-db` separately owns query-only database and stdio MCP access through AWS Identity Center.
|
|
6
|
+
`mesh` remains the repository-local operator and runtime CLI.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Requires Node.js 22 or newer. The product CLI is public on npm and does not
|
|
11
|
+
require GitHub CLI or GitHub Packages access.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install --global @zalosai/zalos-cli@0.2.13
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Installing the CLI does not grant access.
|
|
18
|
+
|
|
19
|
+
## First run
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
zl --environment staging auth login
|
|
23
|
+
# Equivalent regional choices:
|
|
24
|
+
zl --environment us-prod auth login
|
|
25
|
+
zl --environment eu-prod auth login
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`auth login` opens the existing browser sign-in and stores the scoped human session.
|
|
29
|
+
It never changes access-group membership. Database access remains an optional,
|
|
30
|
+
separately distributed developer capability:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm login --scope=@zalos-io --auth-type=legacy --registry=https://npm.pkg.github.com
|
|
34
|
+
npm install --global @zalos-io/developer-db@0.3.2
|
|
35
|
+
zalos-db login --environment staging
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Every remote command requires `--environment staging|us-prod|eu-prod` or an explicit
|
|
39
|
+
`ZL_ENVIRONMENT`. The origins are respectively `dev.app.zalos.ai`, `app.zalos.ai`,
|
|
40
|
+
and `app.eu.zalos.ai`. There is no regional fallback. `--api-url` accepts an explicit
|
|
41
|
+
loopback origin for local development only; `ZL_API_URL` is not an override.
|
|
42
|
+
|
|
43
|
+
Human browser tokens are team-scoped and expire after seven days. `team list` shows
|
|
44
|
+
the active teams and immutable public IDs available to the signed-in user, and
|
|
45
|
+
`team switch <team-ref>` changes
|
|
46
|
+
the selected profile to one of those teams without another browser login. The switch
|
|
47
|
+
preserves the session's original expiry and token scopes. Profiles live at
|
|
48
|
+
`$XDG_CONFIG_HOME/zalos/credentials.json` (default `~/.config/zalos/credentials.json`),
|
|
49
|
+
with directory mode 0700 and file mode 0600. Symbolic links are rejected. There is no
|
|
50
|
+
Keychain integration. Use `--profile` for multiple teams/environments. Legacy profiles
|
|
51
|
+
must be renewed with `auth login`. `auth logout` attempts server revocation and clears
|
|
52
|
+
the local profile even if the server cannot be reached, reporting that failure.
|
|
53
|
+
|
|
54
|
+
AWS portal sessions and permission-set credentials have independent expiry policies.
|
|
55
|
+
When AWS access expires, rerun `zalos-db login --environment <name>`; this does not renew
|
|
56
|
+
your Mesh token. Database reader groups are `zalos-db-staging-readers`,
|
|
57
|
+
`zalos-db-us-prod-readers`, and `zalos-db-eu-prod-readers`.
|
|
58
|
+
|
|
59
|
+
## Product operations
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
export ZL_ENVIRONMENT=staging
|
|
63
|
+
zl workflow list --fields id,name
|
|
64
|
+
zl workflow list --all-teams --limit 100
|
|
65
|
+
zl workflow view <id>
|
|
66
|
+
zl workflow export --all-teams --pretty --output /tmp/mesh-staging-workflows.json
|
|
67
|
+
zl workflow publish <workflow-id> --apply
|
|
68
|
+
zl execution list --limit 1
|
|
69
|
+
zl execution view <id>
|
|
70
|
+
zl execution summary <id>
|
|
71
|
+
zl execution artifact download <artifact-id> --output /tmp/artifact
|
|
72
|
+
zl page list --fields id,title,updatedAt
|
|
73
|
+
zl page export <page-id> --output /tmp/month-end-close.md
|
|
74
|
+
zl page create --input /tmp/month-end-close.md --apply
|
|
75
|
+
zl page update <page-id> --input /tmp/month-end-close.md --apply
|
|
76
|
+
zl workflow run <id> --dry-run
|
|
77
|
+
zl workflow run <id>
|
|
78
|
+
zl template list
|
|
79
|
+
zl credential list
|
|
80
|
+
zl file list
|
|
81
|
+
zl team list
|
|
82
|
+
zl team switch <team-ref>
|
|
83
|
+
zl team list --all-teams
|
|
84
|
+
zl file tree --team-id <team-ref>
|
|
85
|
+
zl file find <query> --team-id <team-ref>
|
|
86
|
+
zl file inspect <file-id> --team-id <team-ref>
|
|
87
|
+
zl file download <file-id> --team-id <team-ref> --output /tmp/file
|
|
88
|
+
zl file snapshot --team-id <team-ref> --output /tmp/team-files.json
|
|
89
|
+
zl file seed --source-team-id <team-ref> --source-path /Fixtures --target-path /Rehearsal
|
|
90
|
+
zl file seed --source-team-id <team-ref> --source-scope generated-outputs --source-path /__mesh/outputs/<execution-id> --target-path /Priors
|
|
91
|
+
zl file seed --source-team-id <team-ref> --source-path /Fixtures --target-path /Rehearsal --apply --expected-checksum <sha256>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Workflow import/update/publish require `--apply`; without it they validate and print a preview.
|
|
95
|
+
`workflow run` executes unless `--dry-run` is supplied. Product authorization comes
|
|
96
|
+
from the signed-in human's team membership, role, and token scopes. Credential reads
|
|
97
|
+
return metadata only. No product command loads database credentials.
|
|
98
|
+
|
|
99
|
+
For protected workflows, `workflow update --apply` saves a draft. Its response reports
|
|
100
|
+
whether runs use that version and names the live version when they do not. Use
|
|
101
|
+
`workflow publish <workflow-id> --apply` to promote the current draft. `workflow export`
|
|
102
|
+
keeps stdout as reusable workflow JSON and writes a structured stderr warning when the
|
|
103
|
+
exported draft differs from the version used by runs.
|
|
104
|
+
|
|
105
|
+
`workflow list --all-teams` and `workflow export --all-teams` read every workflow in
|
|
106
|
+
the selected regional database. They require an internal Zalos human CLI token with
|
|
107
|
+
`workflows:read`; external users and automation tokens are rejected. `workflow export --all`
|
|
108
|
+
remains scoped to the selected profile's team. Run cross-team reads separately
|
|
109
|
+
for `us-prod` and `eu-prod` when both production regions are needed. Cross-team exports
|
|
110
|
+
include each workflow's server checksum so the file can be used with `workflow update`,
|
|
111
|
+
including a content-derived baseline for legacy rows that predate stored checksums.
|
|
112
|
+
|
|
113
|
+
All human users can discover their active memberships with `team list` and select one
|
|
114
|
+
with `team switch <team-ref>`. Prefer the listed immutable `publicId`, such as
|
|
115
|
+
`tm_4ERk9QxW`; legacy internal UUIDs remain accepted. Treat public IDs as opaque strings—
|
|
116
|
+
their suffix length is not an API contract, and they are not database foreign keys.
|
|
117
|
+
Mutating commands then use that selected team context.
|
|
118
|
+
Internal human developers can also discover regional team IDs with `team list --all-teams`
|
|
119
|
+
and use an explicit ID with `file tree`, `find`, `inspect`, `download`, or `snapshot`.
|
|
120
|
+
Downloads request a short-lived URL through the authenticated API and write to a new private
|
|
121
|
+
file; they do not require direct S3 access. File moves remain scoped to the selected team profile.
|
|
122
|
+
Execution artifact downloads use the same private-file and no-overwrite guarantees. Select the
|
|
123
|
+
artifact's team before downloading; the immutable artifact API enforces that team boundary.
|
|
124
|
+
`page export` writes a Markdown process document with a non-rendered revision marker. Edit the
|
|
125
|
+
Markdown, then use `page update --apply`; it refuses an update when the Page changed after export.
|
|
126
|
+
`page create` and `page update` both preview by default and preserve common Tiptap process-document
|
|
127
|
+
structures such as headings, lists, checklists, quotes, code blocks, links, and emphasis.
|
|
128
|
+
Exports reject unsupported inline content, combined marks, and nested or multi-block list items so a
|
|
129
|
+
Markdown edit cannot silently discard Page content.
|
|
130
|
+
`file seed` copies a source file or subtree into the selected team with server-side storage copies.
|
|
131
|
+
Use `--source-scope generated-outputs` to carry prior-period agent outputs into normal Team Files;
|
|
132
|
+
the default source scope is `files`.
|
|
133
|
+
It previews the complete path mapping and checksum by default. Apply the reviewed checksum with
|
|
134
|
+
`--apply --expected-checksum <sha256>`; changed inventories are rejected. The command preserves
|
|
135
|
+
paths below the selected source, creates independent destination files, resumes files already
|
|
136
|
+
copied by the same plan, and blocks on unrelated destination collisions. Switch to the rehearsal
|
|
137
|
+
team before applying the plan.
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
printf 'SELECT current_database()' | zalos-db query --environment staging
|
|
141
|
+
zalos-db mcp --environment eu-prod
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Database queries retain the 500-row ceiling, read-only transaction, five-second
|
|
145
|
+
statement timeout, and rollback. Each MCP process is pinned to one environment.
|
|
146
|
+
|
|
147
|
+
## Automation
|
|
148
|
+
|
|
149
|
+
Use scoped bot tokens injected into `ZL_API_TOKEN` and set `ZL_ENVIRONMENT` explicitly.
|
|
150
|
+
Automation never reads or writes human profiles. Do not save bot tokens with browser
|
|
151
|
+
login or reuse human credentials for unattended work. Existing legacy bot tokens retain
|
|
152
|
+
compatibility during deliberate owner-managed rotation; new tokens use explicit scopes.
|
|
153
|
+
Server-provisioned team bots retain their existing runtime API access through a reserved
|
|
154
|
+
`automation:internal` scope. Only internal team provisioning can issue it; browser CLI
|
|
155
|
+
login and public token creation cannot request it. Team/resource authorization still applies.
|
|
156
|
+
|
|
157
|
+
## Manage Motion
|
|
158
|
+
|
|
159
|
+
Resolve IDs before creating or updating tasks:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
zl project list --fields id,name
|
|
163
|
+
zl state list --project-id <project-id> --fields id,name,type
|
|
164
|
+
zl task list --project-id <project-id> --fields id,title,stateId
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Inspect accepted fields, preview a mutation, and then apply it:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
zl schema task
|
|
171
|
+
|
|
172
|
+
zl task create --project-id <project-id> \
|
|
173
|
+
--json '{"title":"Fix login bug","priority":"high"}' \
|
|
174
|
+
--dry-run
|
|
175
|
+
|
|
176
|
+
zl task create --project-id <project-id> \
|
|
177
|
+
--json '{"title":"Fix login bug","priority":"high"}'
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`zl task view`, `create`, `update`, and `move` include a canonical `url` in their response.
|
|
181
|
+
Move a task without manually reconstructing its project state or link:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
zl task move <task-id> --project-id <target-project-id> --dry-run
|
|
185
|
+
zl task move <task-id> --project-id <target-project-id>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## API tokens
|
|
189
|
+
|
|
190
|
+
Use browser authorization to provision a least-privilege token for any team where the signed-in
|
|
191
|
+
user has an active membership. The browser selects the team and authorizes automatically when its
|
|
192
|
+
session is already active. This uses the product API; it does not require database access, a
|
|
193
|
+
tunnel, or an RDS certificate.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
zl --environment us-prod api-token create \
|
|
197
|
+
--label "CI investigation" \
|
|
198
|
+
--scopes workflows:read,executions:read \
|
|
199
|
+
--expires-at 2026-10-01T00:00:00Z
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Creation returns the plaintext token once. Store it in the approved secret manager immediately;
|
|
203
|
+
the CLI does not persist it as a human profile.
|
|
204
|
+
|
|
205
|
+
Resolve an assignee from the current team, then upload a screenshot as an image attachment:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
zl member list --fields id,name,email
|
|
209
|
+
zl task attachment upload <task-id> ./workflow-unpublished-changes.png
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Task attachments support `png`, `jpeg`, `gif`, `webp`, `avif`, and `svg` images (up to 10 MB).
|
|
213
|
+
`zl project list` excludes archived projects by default; pass `--include-archived` for historical lookup.
|
|
214
|
+
|
|
215
|
+
Run `zl --help` or `zl <resource> --help` for the complete command surface. The available
|
|
216
|
+
resources are `workflow`, `execution`, `template`, `credential`, `file`, `team`, `page`, `task`,
|
|
217
|
+
`project`, `state`, `label`, `cycle`, and `member`; task comments and
|
|
218
|
+
attachments are managed under `zl task`.
|
|
219
|
+
|
|
220
|
+
## Automation contract
|
|
221
|
+
|
|
222
|
+
- Successful output is JSON on stdout.
|
|
223
|
+
- Errors are JSON on stderr and exit with status 1.
|
|
224
|
+
- `--fields id,title,...` limits list and view payloads.
|
|
225
|
+
- `--dry-run` previews supported writes without making an HTTP request.
|
|
226
|
+
- `zl schema [resource]` is local and does not require authentication.
|
|
227
|
+
- `ZL_API_URL` takes precedence over a profile URL, which takes precedence over the
|
|
228
|
+
production default.
|
|
229
|
+
- `ZL_API_TOKEN` takes precedence over the token in the selected profile.
|
|
230
|
+
|
|
231
|
+
Never print tokens in logs or commit `~/.config/zalos/credentials.json`.
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { resolveToken } from './credentials.js';
|
|
2
|
+
import { ApiError, AuthError, NotFoundError } from './utils/errors.js';
|
|
3
|
+
import { currentEnvironment } from './environments.js';
|
|
4
|
+
function isFormData(value) {
|
|
5
|
+
return typeof FormData !== 'undefined' && value instanceof FormData;
|
|
6
|
+
}
|
|
7
|
+
async function responseErrorMessage(response) {
|
|
8
|
+
try {
|
|
9
|
+
const json = (await response.json());
|
|
10
|
+
if (typeof json.message === 'string' && json.message)
|
|
11
|
+
return json.message;
|
|
12
|
+
if (typeof json.error === 'string' && json.error)
|
|
13
|
+
return json.error;
|
|
14
|
+
if (json.error !== null &&
|
|
15
|
+
typeof json.error === 'object' &&
|
|
16
|
+
'message' in json.error &&
|
|
17
|
+
typeof json.error.message === 'string' &&
|
|
18
|
+
json.error.message)
|
|
19
|
+
return json.error.message;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Fall back to the HTTP status text when the response has no JSON message.
|
|
23
|
+
}
|
|
24
|
+
return response.statusText;
|
|
25
|
+
}
|
|
26
|
+
async function request(options, tokenOverride) {
|
|
27
|
+
const { method, path, body, query } = options;
|
|
28
|
+
const baseUrl = currentEnvironment().apiUrl;
|
|
29
|
+
const token = tokenOverride ?? resolveToken();
|
|
30
|
+
const url = new URL(`${baseUrl}/api${path}`);
|
|
31
|
+
if (query) {
|
|
32
|
+
for (const [k, v] of Object.entries(query)) {
|
|
33
|
+
url.searchParams.set(k, v);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const headers = {
|
|
37
|
+
'X-Mesh-API-Key': token,
|
|
38
|
+
};
|
|
39
|
+
if (body != null && !isFormData(body))
|
|
40
|
+
headers['Content-Type'] = 'application/json';
|
|
41
|
+
const response = await fetch(url.toString(), {
|
|
42
|
+
method,
|
|
43
|
+
signal: AbortSignal.timeout(30_000),
|
|
44
|
+
redirect: 'error',
|
|
45
|
+
headers,
|
|
46
|
+
body: body != null ? (isFormData(body) ? body : JSON.stringify(body)) : undefined,
|
|
47
|
+
});
|
|
48
|
+
if (response.status === 401 || response.status === 403) {
|
|
49
|
+
if (options.detailedAuthErrors) {
|
|
50
|
+
const message = await responseErrorMessage(response);
|
|
51
|
+
if (response.status === 401)
|
|
52
|
+
throw new AuthError(message || 'Unauthorized');
|
|
53
|
+
throw new ApiError(response.status, message || 'Forbidden');
|
|
54
|
+
}
|
|
55
|
+
throw new AuthError('Unauthorized. Check your API token.');
|
|
56
|
+
}
|
|
57
|
+
if (response.status === 404) {
|
|
58
|
+
throw new NotFoundError('resource', path);
|
|
59
|
+
}
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const message = await responseErrorMessage(response);
|
|
62
|
+
throw new ApiError(response.status, message);
|
|
63
|
+
}
|
|
64
|
+
if (response.status === 204)
|
|
65
|
+
return undefined;
|
|
66
|
+
return response.json();
|
|
67
|
+
}
|
|
68
|
+
export const client = {
|
|
69
|
+
get: (path, query) => request({ method: 'GET', path, query }),
|
|
70
|
+
post: (path, body, options) => request({ method: 'POST', path, body, ...options }),
|
|
71
|
+
postForm: (path, body) => request({ method: 'POST', path, body }),
|
|
72
|
+
put: (path, body) => request({ method: 'PUT', path, body }),
|
|
73
|
+
patch: (path, body) => request({ method: 'PATCH', path, body }),
|
|
74
|
+
delete: (path) => request({ method: 'DELETE', path }),
|
|
75
|
+
};
|
|
76
|
+
export async function revokeCurrentHumanToken(token) {
|
|
77
|
+
await request({ method: 'DELETE', path: '/auth/cli-token/current' }, token);
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { requestBrowserAuthorization } from './auth.js';
|
|
2
|
+
import { currentEnvironment } from '../environments.js';
|
|
3
|
+
import { AuthError, ValidationError, handleError } from '../utils/errors.js';
|
|
4
|
+
import { revokeBrowserAuthorizedToken } from '../utils/cli-auth.js';
|
|
5
|
+
import { printJson } from '../utils/output.js';
|
|
6
|
+
function parseScopes(raw) {
|
|
7
|
+
const scopes = raw.split(',').map((scope) => scope.trim());
|
|
8
|
+
if (scopes.length === 0 || scopes.some((scope) => !scope))
|
|
9
|
+
throw new ValidationError('scopes', 'Expected a comma-separated list of scopes');
|
|
10
|
+
return scopes;
|
|
11
|
+
}
|
|
12
|
+
function parseExpiry(raw) {
|
|
13
|
+
const expiresAt = raw.trim();
|
|
14
|
+
const time = Date.parse(expiresAt);
|
|
15
|
+
if (!expiresAt || Number.isNaN(time) || time <= Date.now())
|
|
16
|
+
throw new ValidationError('expires-at', 'Expected a future ISO 8601 datetime');
|
|
17
|
+
return new Date(time).toISOString();
|
|
18
|
+
}
|
|
19
|
+
function sameScopes(expected, actual) {
|
|
20
|
+
return (actual !== undefined &&
|
|
21
|
+
expected.length === actual.length &&
|
|
22
|
+
expected.every((scope, index) => scope === actual[index]));
|
|
23
|
+
}
|
|
24
|
+
export async function createApiTokenAction(options) {
|
|
25
|
+
if (process.env.ZL_API_TOKEN)
|
|
26
|
+
throw new AuthError('Unset ZL_API_TOKEN before provisioning a browser-authorized API token');
|
|
27
|
+
const label = options.label.trim();
|
|
28
|
+
if (!label || label.length > 64)
|
|
29
|
+
throw new ValidationError('label', 'Expected a label from 1 to 64 characters');
|
|
30
|
+
const scopes = parseScopes(options.scopes);
|
|
31
|
+
const expiresAt = parseExpiry(options.expiresAt);
|
|
32
|
+
const environment = currentEnvironment();
|
|
33
|
+
const result = await requestBrowserAuthorization({
|
|
34
|
+
tokenName: label,
|
|
35
|
+
scopes,
|
|
36
|
+
expiresAt,
|
|
37
|
+
provisioning: true,
|
|
38
|
+
});
|
|
39
|
+
const confirmed = result.environment === environment.name &&
|
|
40
|
+
result.expiresAt === expiresAt &&
|
|
41
|
+
sameScopes(scopes, result.scopes);
|
|
42
|
+
if (!confirmed) {
|
|
43
|
+
try {
|
|
44
|
+
await revokeBrowserAuthorizedToken(environment.apiUrl, result.token);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new AuthError(`Server did not confirm the requested token scopes and expiry; automatic revocation failed for token ${result.tokenId}`);
|
|
48
|
+
}
|
|
49
|
+
throw new AuthError('Server did not confirm the requested token scopes and expiry');
|
|
50
|
+
}
|
|
51
|
+
printJson({
|
|
52
|
+
token: result.token,
|
|
53
|
+
tokenId: result.tokenId,
|
|
54
|
+
expiresAt: result.expiresAt,
|
|
55
|
+
environment: result.environment,
|
|
56
|
+
scopes: result.scopes,
|
|
57
|
+
profile: result.profile,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function registerApiTokenCommand(program) {
|
|
61
|
+
const command = program
|
|
62
|
+
.command('api-token')
|
|
63
|
+
.description('Provision an API token through browser authorization');
|
|
64
|
+
command
|
|
65
|
+
.command('create')
|
|
66
|
+
.description('Create a least-privilege token for the browser-selected accessible team')
|
|
67
|
+
.requiredOption('--label <name>', 'Human-readable token label (1-64 characters)')
|
|
68
|
+
.requiredOption('--scopes <list>', 'Comma-separated API token scopes')
|
|
69
|
+
.requiredOption('--expires-at <iso-datetime>', 'Future ISO 8601 expiry')
|
|
70
|
+
.action((options) => {
|
|
71
|
+
void createApiTokenAction(options).catch(handleError);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=api-token.js.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { client } from '../client.js';
|
|
2
|
+
import { clearProfile, readProfile, writeProfile } from '../credentials.js';
|
|
3
|
+
import { currentEnvironment } from '../environments.js';
|
|
4
|
+
import { createBrowserProofServer, fetchCliBrowserAuthorizationConfig, generateSession, openBrowser, pollForToken, } from '../utils/cli-auth.js';
|
|
5
|
+
import { AuthError, handleError } from '../utils/errors.js';
|
|
6
|
+
import { printJson } from '../utils/output.js';
|
|
7
|
+
import { dataRow } from '../utils/product-api.js';
|
|
8
|
+
export async function requestBrowserAuthorization(options) {
|
|
9
|
+
const environment = currentEnvironment();
|
|
10
|
+
if (process.env.ZL_API_TOKEN)
|
|
11
|
+
throw new AuthError('Unset ZL_API_TOKEN before browser authorization; automation tokens are never persisted');
|
|
12
|
+
const authConfig = await fetchCliBrowserAuthorizationConfig(environment.apiUrl, environment.name);
|
|
13
|
+
if (options.provisioning) {
|
|
14
|
+
if (!authConfig.capabilities.includes('api-token-provisioning-v1'))
|
|
15
|
+
throw new AuthError('Selected environment does not support browser API token provisioning');
|
|
16
|
+
const invalidScopes = (options.scopes ?? []).filter((scope) => !authConfig.apiTokenScopes.includes(scope));
|
|
17
|
+
if (invalidScopes.length > 0)
|
|
18
|
+
throw new AuthError(`Invalid API token scopes: ${invalidScopes.join(', ')}`);
|
|
19
|
+
}
|
|
20
|
+
const session = await generateSession();
|
|
21
|
+
const proofServer = await createBrowserProofServer(session.sessionId, authConfig.uiOrigin);
|
|
22
|
+
const query = new URLSearchParams({
|
|
23
|
+
session_id: session.sessionId,
|
|
24
|
+
public_key: session.publicKey,
|
|
25
|
+
token_name: options.tokenName,
|
|
26
|
+
environment: environment.name,
|
|
27
|
+
proof_hash: proofServer.browserProofHash,
|
|
28
|
+
callback_port: String(proofServer.callbackPort),
|
|
29
|
+
});
|
|
30
|
+
if (options.scopes) {
|
|
31
|
+
query.set('provision', 'api-token');
|
|
32
|
+
query.set('scopes', options.scopes.join(','));
|
|
33
|
+
}
|
|
34
|
+
if (options.expiresAt)
|
|
35
|
+
query.set('expires_at', options.expiresAt);
|
|
36
|
+
const url = `${environment.apiUrl}/api/auth/cli-login?${query}`;
|
|
37
|
+
try {
|
|
38
|
+
openBrowser(url);
|
|
39
|
+
process.stderr.write(`Open this URL to authorize: ${url}\nWaiting for browser authorization...\n`);
|
|
40
|
+
return await pollForToken(environment.apiUrl, session.sessionId, session.privateKey, 300000);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await proofServer.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function whoamiAction() {
|
|
47
|
+
printJson(await client.get('/user/profile'));
|
|
48
|
+
}
|
|
49
|
+
export async function loginAction(profileName = process.env.ZL_PROFILE ?? 'default') {
|
|
50
|
+
const environment = currentEnvironment();
|
|
51
|
+
const result = await requestBrowserAuthorization({ tokenName: 'zl-cli' });
|
|
52
|
+
if (result.environment !== environment.name)
|
|
53
|
+
throw new AuthError('Login environment mismatch');
|
|
54
|
+
writeProfile(profileName, {
|
|
55
|
+
...result.profile,
|
|
56
|
+
token: result.token,
|
|
57
|
+
tokenId: result.tokenId,
|
|
58
|
+
expiresAt: result.expiresAt,
|
|
59
|
+
environment: environment.name,
|
|
60
|
+
apiUrl: environment.apiUrl,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function verifiedIdentity() {
|
|
64
|
+
const identity = dataRow(await client.get('/user/profile'));
|
|
65
|
+
if (typeof identity.id !== 'string' ||
|
|
66
|
+
!identity.id ||
|
|
67
|
+
typeof identity.email !== 'string' ||
|
|
68
|
+
(identity.name !== null && typeof identity.name !== 'string'))
|
|
69
|
+
throw new AuthError('Invalid identity response');
|
|
70
|
+
return { id: identity.id, email: identity.email, name: identity.name };
|
|
71
|
+
}
|
|
72
|
+
export async function authStatus() {
|
|
73
|
+
const environment = currentEnvironment();
|
|
74
|
+
if (process.env.ZL_API_TOKEN) {
|
|
75
|
+
const identity = await verifiedIdentity();
|
|
76
|
+
return {
|
|
77
|
+
authenticated: true,
|
|
78
|
+
source: 'environment',
|
|
79
|
+
environment: environment.name,
|
|
80
|
+
apiUrl: environment.apiUrl,
|
|
81
|
+
user: { id: identity.id, email: identity.email, name: identity.name },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const profile = readProfile(process.env.ZL_PROFILE ?? 'default');
|
|
85
|
+
if (!profile)
|
|
86
|
+
return {
|
|
87
|
+
authenticated: false,
|
|
88
|
+
environment: environment.name,
|
|
89
|
+
apiUrl: environment.apiUrl,
|
|
90
|
+
};
|
|
91
|
+
currentEnvironment(profile.environment);
|
|
92
|
+
const identity = await verifiedIdentity();
|
|
93
|
+
return {
|
|
94
|
+
authenticated: true,
|
|
95
|
+
environment: environment.name,
|
|
96
|
+
apiUrl: environment.apiUrl,
|
|
97
|
+
tokenId: profile.tokenId,
|
|
98
|
+
expiresAt: profile.expiresAt,
|
|
99
|
+
remainingSeconds: Math.max(0, Math.floor((Date.parse(profile.expiresAt) - Date.now()) / 1000)),
|
|
100
|
+
user: identity,
|
|
101
|
+
organization: profile.organization,
|
|
102
|
+
team: profile.team,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export async function logoutAction(profileName) {
|
|
106
|
+
currentEnvironment();
|
|
107
|
+
if (process.env.ZL_API_TOKEN)
|
|
108
|
+
throw new AuthError('Unset ZL_API_TOKEN to log out a human profile');
|
|
109
|
+
let profile;
|
|
110
|
+
try {
|
|
111
|
+
profile = readProfile(profileName);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// clearProfile rechecks the path and file shape, so unsafe files still fail closed.
|
|
115
|
+
clearProfile(profileName);
|
|
116
|
+
process.stderr.write('Warning: invalid legacy profile removed locally; its remote token could not be revoked. Revoke it in Zalos account settings.\n');
|
|
117
|
+
printJson({ message: 'logged out', profile: profileName });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (profile) {
|
|
121
|
+
currentEnvironment(profile.environment);
|
|
122
|
+
const previous = process.env.ZL_PROFILE;
|
|
123
|
+
process.env.ZL_PROFILE = profileName;
|
|
124
|
+
try {
|
|
125
|
+
await client.delete('/auth/cli-token/current');
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
process.stderr.write('Warning: remote revocation failed; token may remain valid until expiry.\n');
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
if (previous === undefined)
|
|
132
|
+
delete process.env.ZL_PROFILE;
|
|
133
|
+
else
|
|
134
|
+
process.env.ZL_PROFILE = previous;
|
|
135
|
+
clearProfile(profileName);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
printJson({ message: 'logged out', profile: profileName });
|
|
139
|
+
}
|
|
140
|
+
export function registerAuthCommand(program) {
|
|
141
|
+
const auth = program
|
|
142
|
+
.command('auth')
|
|
143
|
+
.description('Manage human authentication; CI uses ZL_API_TOKEN');
|
|
144
|
+
auth
|
|
145
|
+
.command('login')
|
|
146
|
+
.description('Sign in through the browser for seven days')
|
|
147
|
+
.action(async () => {
|
|
148
|
+
try {
|
|
149
|
+
await loginAction();
|
|
150
|
+
printJson({
|
|
151
|
+
message: 'Logged in',
|
|
152
|
+
profile: process.env.ZL_PROFILE ?? 'default',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
handleError(error);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
auth
|
|
160
|
+
.command('logout')
|
|
161
|
+
.description('Revoke current human token and clear local credentials')
|
|
162
|
+
.action(async () => {
|
|
163
|
+
try {
|
|
164
|
+
await logoutAction(process.env.ZL_PROFILE ?? 'default');
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
handleError(error);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
auth
|
|
171
|
+
.command('status')
|
|
172
|
+
.description('Verify identity and report session lifetime')
|
|
173
|
+
.action(async () => {
|
|
174
|
+
try {
|
|
175
|
+
const status = await authStatus();
|
|
176
|
+
printJson(status);
|
|
177
|
+
if (!status.authenticated)
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
handleError(error);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
auth
|
|
185
|
+
.command('whoami')
|
|
186
|
+
.description('Print current user profile')
|
|
187
|
+
.action(async () => {
|
|
188
|
+
try {
|
|
189
|
+
await whoamiAction();
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
handleError(error);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=auth.js.map
|