@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 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