@tiangong-lca/cli 0.1.3 → 0.1.5

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.
Files changed (36) hide show
  1. package/README.md +43 -11
  2. package/dist/src/cli.d.ts +4 -0
  3. package/dist/src/cli.js +264 -7
  4. package/dist/src/cli.js.map +1 -1
  5. package/dist/src/lib/auth-identity-receipt.d.ts +2 -2
  6. package/dist/src/lib/auth-identity-receipt.js +20 -12
  7. package/dist/src/lib/auth-identity-receipt.js.map +1 -1
  8. package/dist/src/lib/env.d.ts +8 -0
  9. package/dist/src/lib/env.js +36 -3
  10. package/dist/src/lib/env.js.map +1 -1
  11. package/dist/src/lib/lca-release.d.ts +1 -1
  12. package/dist/src/lib/lca-release.js +3 -4
  13. package/dist/src/lib/lca-release.js.map +1 -1
  14. package/dist/src/lib/lifecyclemodel-resulting-process.js +6 -2
  15. package/dist/src/lib/lifecyclemodel-resulting-process.js.map +1 -1
  16. package/dist/src/lib/oauth-loopback.d.ts +52 -0
  17. package/dist/src/lib/oauth-loopback.js +229 -0
  18. package/dist/src/lib/oauth-loopback.js.map +1 -0
  19. package/dist/src/lib/oauth-pkce.d.ts +63 -0
  20. package/dist/src/lib/oauth-pkce.js +268 -0
  21. package/dist/src/lib/oauth-pkce.js.map +1 -0
  22. package/dist/src/lib/process-refresh-references.js +2 -2
  23. package/dist/src/lib/process-refresh-references.js.map +1 -1
  24. package/dist/src/lib/process-scope-statistics.js +8 -3
  25. package/dist/src/lib/process-scope-statistics.js.map +1 -1
  26. package/dist/src/lib/state-lock.js +10 -4
  27. package/dist/src/lib/state-lock.js.map +1 -1
  28. package/dist/src/lib/supabase-client.d.ts +13 -1
  29. package/dist/src/lib/supabase-client.js +64 -3
  30. package/dist/src/lib/supabase-client.js.map +1 -1
  31. package/dist/src/lib/supabase-json-ordered-write.js +7 -3
  32. package/dist/src/lib/supabase-json-ordered-write.js.map +1 -1
  33. package/dist/src/lib/supabase-session.d.ts +63 -8
  34. package/dist/src/lib/supabase-session.js +315 -21
  35. package/dist/src/lib/supabase-session.js.map +1 -1
  36. package/package.json +1 -1
package/README.md CHANGED
@@ -13,6 +13,7 @@ whenToUpdate:
13
13
  - when user-facing command examples change
14
14
  checkPaths:
15
15
  - README.md
16
+ - .env.example
16
17
  - package.json
17
18
  - pnpm-workspace.yaml
18
19
  - pnpm-lock.yaml
@@ -23,19 +24,22 @@ checkPaths:
23
24
  - src/batch.ts
24
25
  - src/command-spec.ts
25
26
  - src/lib/auth-identity-receipt.ts
27
+ - src/lib/oauth-loopback.ts
28
+ - src/lib/oauth-pkce.ts
29
+ - src/lib/supabase-session.ts
26
30
  - src/lib/lca-release.ts
27
31
  - scripts/run-auth-identity-production-case.ts
28
32
  - test/auth-identity*.test.ts
29
33
  - test/public-auth-identity-receipt.test.ts
30
34
  - test/lca-release*.test.ts
31
- lastReviewedAt: 2026-08-29
32
- lastReviewedCommit: f460f0567faac6e89e53d259fbd29d1dfccd058d
33
- lastReviewedNote: 'Reviewed for Issue #242: identifies 0.1.3 as the release-only package identity for the public auth receipt parser while runtime, exports, dependencies, and release behavior stay fixed.'
35
+ lastReviewedAt: 2026-08-31
36
+ lastReviewedCommit: 352c11c5ffafaea1bf94133be92f3337b847866a
37
+ lastReviewedNote: 'Reviewed for Issue #252: identifies 0.1.5 as the replacement OAuth release after unpublished 0.1.4, preserving exports, dependencies, and release behavior.'
34
38
  ---
35
39
 
36
40
  # TianGong LCA CLI
37
41
 
38
- Package: `@tiangong-lca/cli` Executable: `tiangong-lca` Current package version: `0.1.3` Node: `24.19.0`
42
+ Package: `@tiangong-lca/cli` Executable: `tiangong-lca` Current package version: `0.1.5` Node: `24.19.0`
39
43
 
40
44
  Repository development is single-track on pnpm `11.24.0` and TypeScript `7.0.2`. The published package remains a clean, package-manager-neutral consumer artifact: it contains runtime files only, not pnpm, TypeScript, Oxlint, tests, source-only tooling, or repository lockfiles.
41
45
 
@@ -59,6 +63,14 @@ Review note, 2026-08-29: Issue #240 adds the typed `@tiangong-lca/cli/auth-ident
59
63
 
60
64
  Review note, 2026-08-29: Issue #242 releases that public parser as `@tiangong-lca/cli@0.1.3`. Only package identity and four existing exact-version fixtures change; `./batch`, `./command-spec`, `./auth-identity-receipt`, the executable, dependencies, pnpm lock, Node 24.19.0 / TypeScript 7.0.2 toolchain, and package-manager-neutral consumer contract are otherwise unchanged.
61
65
 
66
+ Review note, 2026-08-31: Issue #244 adds Supabase OAuth 2.1 Authorization Code + S256 PKCE without adding a dependency or changing package version/exports. `auth login` uses a registered public client, exact literal-`127.0.0.1` callback, state validation, and shell-free system browser; it atomically stores rotating access/refresh tokens in a private session file. `auth status` performs no network request and reveals no credential or session path; `auth whoami` and `auth doctor-auth` use the live redacted identity receipt. `auth logout` deletes only that matching local session; Connected applications owns grant revocation. `TIANGONG_LCA_ACCESS_TOKEN` is a short-lived, online-verified, memory-only headless option. The reversible API key remains only as a transition fallback and is never used by OAuth mode.
67
+
68
+ Review note, 2026-08-31: Issue #247 prevents a second CLI process from failing when the first process removes `session.json.lock` between contention and metadata read. Only an absent lock is treated as released; permission, I/O, malformed-owner, timeout, and stale-owner safety behavior is unchanged.
69
+
70
+ Review note, 2026-08-31: Issue #246 releases that OAuth runtime as `@tiangong-lca/cli@0.1.4`. Only package identity and four existing exact-version fixtures change; the executable and public subpaths, dependencies, pnpm lock, Node 24.19.0 / TypeScript 7.0.2 toolchain, auth behavior, and package-manager-neutral consumer contract are otherwise unchanged.
71
+
72
+ Review note, 2026-08-31: Issue #252 replaces the stopped-unpublished 0.1.4 attempt with `@tiangong-lca/cli@0.1.5` after platform-complete session coverage merged. Only package identity, four version fixtures, and release evidence change; executable/public subpaths, OAuth behavior, dependencies, pnpm lock, Node/TypeScript toolchain, and consumer contract stay unchanged.
73
+
62
74
  Review note, 2026-07-12: `dataset maintenance plan/apply/verify` provides current-user RLS-scoped exact-row maintenance with immutable plans, explicit approval, per-action logs, platform audit correlation, and independent readback. `merge-support-aliases` now runs only in `target_mode=owner_draft`: source/target support and all changed rows stay private `state_code=0`; publication is a separate future workflow.
63
75
 
64
76
  Review note, 2026-07-13: maintenance scans now prove exact-count pagination even when PostgREST returns fewer rows than the requested `--page-size`. An incomplete or inconsistent scan fails before artifacts, approval, or mutation; under stable filtered membership/order the proof represents a complete ordered multi-request traversal, not one transaction-level/MVCC snapshot.
@@ -127,25 +139,45 @@ Remote commands require:
127
139
 
128
140
  ```bash
129
141
  TIANGONG_LCA_API_BASE_URL=
130
- TIANGONG_LCA_API_KEY=
131
142
  TIANGONG_LCA_SUPABASE_PUBLISHABLE_KEY=
143
+ TIANGONG_LCA_OAUTH_CLIENT_ID=
132
144
  TIANGONG_LCA_REGION=us-east-1
133
145
  ```
134
146
 
135
147
  Notes:
136
148
 
137
149
  - `TIANGONG_LCA_API_BASE_URL` accepts the project root, `/functions/v1`, or `/rest/v1`.
138
- - `TIANGONG_LCA_API_KEY` is the TianGong user API key from the account page, not a Supabase project key.
139
- - The CLI exchanges `TIANGONG_LCA_API_KEY` for a user session, then reuses the access token for both Edge Functions and direct Supabase access.
150
+ - `TIANGONG_LCA_OAUTH_CLIENT_ID` is the environment-specific registered public CLI client; it is not a secret.
151
+ - Run `tiangong-lca auth login` once in a trusted terminal. All Edge Function and direct Supabase commands then reuse the OAuth access token and rotate the refresh token on demand.
152
+ - For approved headless execution, set `TIANGONG_LCA_AUTH_MODE=access-token` and inject one short-lived `TIANGONG_LCA_ACCESS_TOKEN`. It is verified online, kept only in process memory, and never refreshed.
153
+ - Transition-only compatibility uses `TIANGONG_LCA_AUTH_MODE=legacy-user-api-key` plus `TIANGONG_LCA_API_KEY`. OAuth failures never fall back to this password-equivalent credential.
140
154
 
141
155
  Optional session control:
142
156
 
143
157
  ```bash
158
+ TIANGONG_LCA_AUTH_MODE=oauth
159
+ TIANGONG_LCA_OAUTH_REDIRECT_URI=http://127.0.0.1:49191/oauth/callback
144
160
  TIANGONG_LCA_SESSION_FILE=
145
161
  TIANGONG_LCA_DISABLE_SESSION_CACHE=false
146
162
  TIANGONG_LCA_FORCE_REAUTH=false
147
163
  ```
148
164
 
165
+ ## OAuth Session Commands
166
+
167
+ ```text
168
+ tiangong-lca auth login
169
+ tiangong-lca auth status --json
170
+ tiangong-lca auth whoami --json
171
+ tiangong-lca auth doctor-auth --json
172
+ tiangong-lca auth logout
173
+ ```
174
+
175
+ The callback URI must exactly match the URI registered with the selected Supabase OAuth client. The default is `http://127.0.0.1:49191/oauth/callback`; OAuth client redirect URIs do not support wildcards. Login never accepts a username, password, authorization code, access token, refresh token, or PKCE verifier through argv. On POSIX, the app directory is `0700` and `session.json` is `0600`; writes and refresh-token rotation use a temporary file, atomic rename, and the existing cross-process state lock. Windows callers must keep the selected parent ACL current-user-only because chmod bits are not an ACL.
176
+
177
+ `auth status` is intentionally local-only and non-mutating. It reports whether a matching session can be used or refreshed, but sets `onlineVerified: false`; it never prints email, tokens, a session path, or a credential fingerprint. `auth whoami` performs the live redacted identity receipt. `auth doctor-auth` first checks local readiness, then performs that live check; a missing OAuth session returns `login-required` so a human can run `auth login`. An AI agent must never ask for or handle the user's password, authorization code, access token, or refresh token.
178
+
179
+ Local logout does not revoke the server grant. To invalidate every refresh token for the CLI client, open Account → Connected applications and disconnect TianGong CLI.
180
+
149
181
  ## Auth Identity Receipt
150
182
 
151
183
  Use the identity receipt before a production-backed case or any later owner-draft write gate:
@@ -154,7 +186,7 @@ Use the identity receipt before a production-backed case or any later owner-draf
154
186
  tiangong-lca auth identity-receipt --expected-project-ref <project-ref> --expected-user-id <user-id> --json
155
187
  ```
156
188
 
157
- The command performs no dataset write. It exchanges the normal user API key for a session, checks the canonical Supabase project, and makes a bounded live `GET /auth/v1/user`. A cached token that receives `401` or `403` may be refreshed and retried exactly once; all other transport or response failures are terminal. A valid production guard requires:
189
+ The command performs no dataset write. It resolves the selected OAuth, explicit headless, or transition-only legacy session, checks the canonical Supabase project, and makes a bounded live `GET /auth/v1/user`. A refreshable cached token that receives `401` or `403` may be refreshed and retried exactly once; an explicit headless access token is never refreshed. All other transport or response failures are terminal. A valid production guard requires:
158
190
 
159
191
  - `schema` exactly `tiangong-lca.auth-identity-receipt.v1`;
160
192
  - `status: "passed"`, `operation: "current-user-read"`, and `remote_write_mode: "read-only"`;
@@ -169,7 +201,7 @@ For the explicitly authorized local production read case from a validated reposi
169
201
  pnpm case:auth-identity:production -- --env-file <data-foundry-ignored-.env> --expected-project-ref <project-ref> --expected-user-id <user-id> --out-dir <new-private-case-directory>
170
202
  ```
171
203
 
172
- The runner reads only `TIANGONG_LCA_API_BASE_URL`, `TIANGONG_LCA_SUPABASE_PUBLISHABLE_KEY`, and `TIANGONG_LCA_TEST_API_KEY`; the last is mapped to the child process's standard API-key variable. It does not accept an alternate CLI path. The pnpm command first performs a clean TS7 build without the production env. Its plain-Node runner then single-reads source/config/lock and the freshly generated `dist/src/**/*.js`, hashes the source tree, runner, runtime, exact entrypoint, and pnpm lock, and copies those exact built buffers into a private snapshot before exposing the key. It forces reauthentication with session cache disabled, runs only the built snapshot from an exclusively created clean directory with an argv array and `shell:false`, cleans the snapshot before publishing success artifacts, and persists only the parsed receipt and case manifest. POSIX creates the case directory as `0700` and files as `0600`; Windows inherits ACLs from the caller-selected parent, so use a user-restricted parent because mode bits are not an ACL guarantee. The runner never stores raw child stdout/stderr and is intentionally not wired to CI secrets. This receipt is locally hash-verifiable, not server-signed attestation.
204
+ The historical production-case runner remains a transition-only legacy fixture: it reads only `TIANGONG_LCA_API_BASE_URL`, `TIANGONG_LCA_SUPABASE_PUBLISHABLE_KEY`, and `TIANGONG_LCA_TEST_API_KEY`; the last is mapped to the child process's explicit legacy variable. It does not accept an alternate CLI path. The pnpm command first performs a clean TS7 build without the production env. Its plain-Node runner then single-reads source/config/lock and the freshly generated `dist/src/**/*.js`, hashes the source tree, runner, runtime, exact entrypoint, and pnpm lock, and copies those exact built buffers into a private snapshot before exposing the key. It forces reauthentication with session cache disabled, runs only the built snapshot from an exclusively created clean directory with an argv array and `shell:false`, cleans the snapshot before publishing success artifacts, and persists only the parsed receipt and case manifest. POSIX creates the case directory as `0700` and files as `0600`; Windows inherits ACLs from the caller-selected parent, so use a user-restricted parent because mode bits are not an ACL guarantee. The runner never stores raw child stdout/stderr and is intentionally not wired to CI secrets. New automation should use OAuth/headless mode instead. This receipt is locally hash-verifiable, not server-signed attestation.
173
205
 
174
206
  ## LCI/LCIA Data Release
175
207
 
@@ -235,7 +267,7 @@ tiangong-lca release status --release-run-id <release-run-id> --json
235
267
  }
236
268
  ```
237
269
 
238
- The CLI verifies every local upload against its declared byte size, SHA-256, media type, and required pair before requesting signed URLs. Upload receipts and downloads are written atomically with private file permissions. Existing outputs are preserved unless `--force` is explicit. The publish credential fingerprint is derived locally from `TIANGONG_LCA_API_KEY`; callers cannot inject it through the request file.
270
+ The CLI verifies every local upload against its declared byte size, SHA-256, media type, and required pair before requesting signed URLs. Upload receipts and downloads are written atomically with private file permissions. Existing outputs are preserved unless `--force` is explicit. Release requests use the resolved actor access token and `client_id`-aware database policy; no credential-derived fingerprint is accepted from local state or the request file.
239
271
 
240
272
  Calculation results and published artifacts remain file-first:
241
273
 
@@ -325,7 +357,7 @@ Key outputs under `--out-dir`:
325
357
  - `outputs/identity-candidates.jsonl`
326
358
  - `outputs/identity-candidate-sources.json`
327
359
 
328
- `--candidate-input` is repeatable and accepts JSON, JSONL, or a directory scanned recursively for JSON/JSONL candidate rows. Embedded `candidates` from the request and local-scan candidates are evaluated together. Add `--remote-candidates` when the preflight should also call `process_hybrid_search` or `flow_hybrid_search`; `--remote-query` overrides the target-derived search text and `--remote-limit` caps returned candidate rows. Remote candidate search uses the normal Supabase session env: `TIANGONG_LCA_API_BASE_URL`, `TIANGONG_LCA_API_KEY`, `TIANGONG_LCA_SUPABASE_PUBLISHABLE_KEY`, and optional `TIANGONG_LCA_REGION`.
360
+ `--candidate-input` is repeatable and accepts JSON, JSONL, or a directory scanned recursively for JSON/JSONL candidate rows. Embedded `candidates` from the request and local-scan candidates are evaluated together. Add `--remote-candidates` when the preflight should also call `process_hybrid_search` or `flow_hybrid_search`; `--remote-query` overrides the target-derived search text and `--remote-limit` caps returned candidate rows. Remote candidate search uses the normal OAuth session env above and optional `TIANGONG_LCA_REGION`.
329
361
 
330
362
  When remote search is enabled, the CLI sends a compact fielded `query` string plus supported edge-search options to `process_hybrid_search` or `flow_hybrid_search`: `filter`, `match_count`, `page_size`, `data_source`, `match_threshold`, `lexical_weight`, `semantic_weight`, and `rrf_k`. `lexical_weight` controls the single database-owned `extracted_md` lexical branch. Request-level `remote_candidate_search.profile_hints` are not sent to the Edge Function. They are applied locally before scoring candidates so Foundry can provide source-derived facts such as flow type, flow property, reference unit, elementary categories, geography, reference-flow names, technology route, and system boundary without polluting the full-text/semantic query.
331
363
 
package/dist/src/cli.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { DotEnvLoadResult } from './lib/dotenv.js';
2
2
  import type { FetchLike } from './lib/http.js';
3
+ import { inspectSupabaseAuthStatus, loginWithSupabaseOAuth, logoutSupabaseUserSession } from './lib/supabase-session.js';
3
4
  import { type AuthIdentityReceipt, type RunAuthIdentityReceiptOptions } from './lib/auth-identity-receipt.js';
4
5
  import { type LifecyclemodelAutoBuildReport, type RunLifecyclemodelAutoBuildOptions } from './lib/lifecyclemodel-auto-build.js';
5
6
  import { type LifecyclemodelResultingProcessReport, type RunLifecyclemodelResultingProcessOptions } from './lib/lifecyclemodel-resulting-process.js';
@@ -71,6 +72,9 @@ export type CliDeps = {
71
72
  dotEnvStatus: DotEnvLoadResult;
72
73
  fetchImpl: FetchLike;
73
74
  runAuthIdentityReceiptImpl?: (options: RunAuthIdentityReceiptOptions) => Promise<AuthIdentityReceipt>;
75
+ loginWithSupabaseOAuthImpl?: typeof loginWithSupabaseOAuth;
76
+ logoutSupabaseUserSessionImpl?: typeof logoutSupabaseUserSession;
77
+ inspectSupabaseAuthStatusImpl?: typeof inspectSupabaseAuthStatus;
74
78
  runPublishImpl?: (options: RunPublishOptions) => Promise<PublishReport>;
75
79
  runValidationImpl?: (options: RunValidationOptions) => Promise<ValidationRunReport>;
76
80
  runLcaReleaseImpl?: (options: RunLcaReleaseOptions) => Promise<LcaReleaseReport>;
package/dist/src/cli.js CHANGED
@@ -3,6 +3,9 @@ import { buildDoctorReport, readRuntimeEnv } from './lib/env.js';
3
3
  import { CliError, toErrorPayload } from './lib/errors.js';
4
4
  import { stringifyJson } from './lib/io.js';
5
5
  import { loadCliPackageVersion } from './lib/package-version.js';
6
+ import { requireSupabaseRestRuntime } from './lib/supabase-client.js';
7
+ import { inspectSupabaseAuthStatus, loginWithSupabaseOAuth, logoutSupabaseUserSession, } from './lib/supabase-session.js';
8
+ import { OAUTH_LOGIN_TIMEOUT_MAX_MS } from './lib/oauth-loopback.js';
6
9
  import { AUTH_IDENTITY_MAX_TIMEOUT_MS, AUTH_IDENTITY_RECEIPT_SCHEMA, runAuthIdentityReceipt, } from './lib/auth-identity-receipt.js';
7
10
  import { runLifecyclemodelAutoBuild, } from './lib/lifecyclemodel-auto-build.js';
8
11
  import { runLifecyclemodelBuildResultingProcess, } from './lib/lifecyclemodel-resulting-process.js';
@@ -87,7 +90,7 @@ Usage:
87
90
  Commands:
88
91
  Implemented Commands:
89
92
  doctor show environment diagnostics
90
- auth identity-receipt
93
+ auth login | status | whoami | doctor-auth | logout | identity-receipt
91
94
  search flow | process | lifecyclemodel
92
95
  process get | list | identity-preflight | build-plan | scope-statistics | dedup-review | auto-build | resume-build | publish-build | complete-required-fields | save-draft | batch-build | refresh-references | verify-rows
93
96
  dataset contract get | context-pack | classification children/path/audit/apply | curation-queue build/next/verify | import-lca convert | author | patch apply | save-draft | source upload-attachments | validate | verify-remote | bilingual extract/apply/validate | evidence-search plan/run | references rewrite/refresh-remote | maintenance clear-account/plan/apply/verify/flow-identity
@@ -100,13 +103,17 @@ Implemented Commands:
100
103
  admin embedding-run
101
104
 
102
105
  Planned Surface (not implemented yet):
103
- auth whoami | doctor-auth
104
106
  job get | wait | logs
105
107
 
106
108
  Planned commands currently print an explicit "not implemented yet" message and exit with code 2.
107
109
 
108
110
  Examples:
109
111
  tiangong-lca doctor
112
+ tiangong-lca auth login
113
+ tiangong-lca auth status --json
114
+ tiangong-lca auth whoami --json
115
+ tiangong-lca auth doctor-auth --json
116
+ tiangong-lca auth logout
110
117
  tiangong-lca auth identity-receipt --expected-project-ref <project-ref> --expected-user-id <user-id> --json
111
118
  tiangong-lca search flow --input ./request.json
112
119
  tiangong-lca search process --input ./request.json --dry-run
@@ -185,15 +192,79 @@ Environment:
185
192
  }
186
193
  function renderAuthHelp() {
187
194
  return `Usage:
188
- tiangong-lca auth identity-receipt [options]
195
+ tiangong-lca auth <login|status|whoami|doctor-auth|logout|identity-receipt> [options]
189
196
 
190
197
  Implemented Subcommands:
198
+ login Authorize the registered public CLI through browser PKCE and store a private rotating session
199
+ status Inspect only local auth readiness without reading or printing credentials
200
+ whoami Run the live, redacted identity receipt for the current user
201
+ doctor-auth Combine local readiness with a live, redacted identity check
202
+ logout Remove only this local CLI session; revoke the connected-app grant from the account page
191
203
  identity-receipt Live, read-only proof of the authenticated account and Supabase project
192
204
 
193
- Planned Subcommands:
194
- whoami | doctor-auth
205
+ Use "tiangong-lca auth <subcommand> --help" for details.
206
+ `.trim();
207
+ }
208
+ function renderAuthStatusHelp() {
209
+ return `Usage:
210
+ tiangong-lca auth status [--json]
211
+
212
+ Inspects only the matching local session metadata. It performs no network request, never refreshes
213
+ a token, and never emits email, token, session path, or credential fingerprint. A ready result can
214
+ still be remotely revoked; use "auth doctor-auth" when live verification is required. If OAuth login
215
+ is required, hand the terminal to the human user and run "tiangong-lca auth login". Never ask an AI
216
+ agent to collect a username, password, authorization code, access token, or refresh token.
217
+ `.trim();
218
+ }
219
+ function renderAuthWhoamiHelp() {
220
+ return `Usage:
221
+ tiangong-lca auth whoami [--timeout-ms <n>] [--json]
222
+
223
+ Runs the same live, read-only and redacted proof as "auth identity-receipt" without requiring an
224
+ expected identity assertion. It may refresh an OAuth session under the private session lock. Output
225
+ contains no token, full email address, session path, or credential fingerprint.
226
+ `.trim();
227
+ }
228
+ function renderAuthDoctorHelp() {
229
+ return `Usage:
230
+ tiangong-lca auth doctor-auth [--timeout-ms <n>] [--json]
231
+
232
+ First checks local readiness without network access. If ready, it performs the live redacted identity
233
+ receipt. Missing OAuth authorization returns login-required without attempting password bootstrap.
234
+ Run "tiangong-lca auth login" only in a trusted human-controlled terminal.
235
+ `.trim();
236
+ }
237
+ function renderAuthLoginHelp() {
238
+ return `Usage:
239
+ tiangong-lca auth login [options]
240
+
241
+ Options:
242
+ --timeout-ms <n> Browser callback timeout 1..${OAUTH_LOGIN_TIMEOUT_MAX_MS} ms (default: 180000)
243
+ --json Print compact JSON; default output is pretty JSON
244
+ -h, --help
245
+
246
+ Required env:
247
+ TIANGONG_LCA_API_BASE_URL
248
+ TIANGONG_LCA_SUPABASE_PUBLISHABLE_KEY
249
+ TIANGONG_LCA_OAUTH_CLIENT_ID
250
+
251
+ Optional env:
252
+ TIANGONG_LCA_AUTH_MODE=oauth
253
+ TIANGONG_LCA_OAUTH_REDIRECT_URI (default: exact registered 127.0.0.1 callback)
254
+ TIANGONG_LCA_SESSION_FILE
255
+
256
+ The CLI binds only 127.0.0.1, opens the system browser without a shell, verifies state and S256
257
+ PKCE, and atomically writes access/rotating refresh tokens to a private session file. It never
258
+ accepts a username, password, authorization code, access token, or refresh token as an argv value.
259
+ `.trim();
260
+ }
261
+ function renderAuthLogoutHelp() {
262
+ return `Usage:
263
+ tiangong-lca auth logout [--json]
195
264
 
196
- Use "tiangong-lca auth identity-receipt --help" for the machine-receipt contract.
265
+ Removes only the locally cached session bound to the current project/client. To revoke the OAuth
266
+ grant and every refresh token, disconnect TianGong CLI from Connected applications in the account
267
+ page. An explicit TIANGONG_LCA_ACCESS_TOKEN is never persisted and therefore has nothing to delete.
197
268
  `.trim();
198
269
  }
199
270
  function renderAuthIdentityReceiptHelp() {
@@ -218,8 +289,14 @@ Safety:
218
289
 
219
290
  Required env:
220
291
  TIANGONG_LCA_API_BASE_URL
221
- TIANGONG_LCA_API_KEY
222
292
  TIANGONG_LCA_SUPABASE_PUBLISHABLE_KEY
293
+ TIANGONG_LCA_OAUTH_CLIENT_ID and a prior "tiangong-lca auth login"
294
+
295
+ Headless alternative:
296
+ TIANGONG_LCA_ACCESS_TOKEN (short-lived actor token; never cached or refreshed)
297
+
298
+ Legacy compatibility:
299
+ TIANGONG_LCA_AUTH_MODE=legacy-user-api-key plus TIANGONG_LCA_API_KEY
223
300
 
224
301
  Optional session env:
225
302
  TIANGONG_LCA_SESSION_FILE
@@ -2369,6 +2446,99 @@ function parseAuthIdentityReceiptFlags(args) {
2369
2446
  timeoutMs,
2370
2447
  };
2371
2448
  }
2449
+ function parseAuthLoginFlags(args) {
2450
+ let parsed;
2451
+ try {
2452
+ parsed = parseArgs({
2453
+ args,
2454
+ allowPositionals: false,
2455
+ strict: true,
2456
+ tokens: true,
2457
+ options: {
2458
+ help: { type: 'boolean', short: 'h' },
2459
+ json: { type: 'boolean' },
2460
+ 'timeout-ms': { type: 'string' },
2461
+ },
2462
+ });
2463
+ }
2464
+ catch (error) {
2465
+ throw new CliError(String(error), { code: 'INVALID_ARGS', exitCode: 2 });
2466
+ }
2467
+ const timeoutOccurrences = parsed.tokens.filter((entry) => entry.kind === 'option' && entry.name === 'timeout-ms').length;
2468
+ if (timeoutOccurrences > 1) {
2469
+ throw new CliError('Option --timeout-ms may be provided only once.', {
2470
+ code: 'INVALID_ARGS',
2471
+ exitCode: 2,
2472
+ });
2473
+ }
2474
+ const timeoutText = parsed.values['timeout-ms'];
2475
+ const timeoutMs = typeof timeoutText === 'string' ? Number(timeoutText) : 180_000;
2476
+ if (!Number.isSafeInteger(timeoutMs) ||
2477
+ timeoutMs <= 0 ||
2478
+ timeoutMs > OAUTH_LOGIN_TIMEOUT_MAX_MS) {
2479
+ throw new CliError(`Expected --timeout-ms to be an integer between 1 and ${OAUTH_LOGIN_TIMEOUT_MAX_MS}.`, { code: 'INVALID_TIMEOUT', exitCode: 2 });
2480
+ }
2481
+ return {
2482
+ help: Boolean(parsed.values.help),
2483
+ json: Boolean(parsed.values.json),
2484
+ timeoutMs,
2485
+ };
2486
+ }
2487
+ function parseAuthLogoutFlags(args) {
2488
+ try {
2489
+ const { values } = parseArgs({
2490
+ args,
2491
+ allowPositionals: false,
2492
+ strict: true,
2493
+ options: {
2494
+ help: { type: 'boolean', short: 'h' },
2495
+ json: { type: 'boolean' },
2496
+ },
2497
+ });
2498
+ return { help: Boolean(values.help), json: Boolean(values.json) };
2499
+ }
2500
+ catch (error) {
2501
+ throw new CliError(String(error), { code: 'INVALID_ARGS', exitCode: 2 });
2502
+ }
2503
+ }
2504
+ function parseAuthLiveFlags(args) {
2505
+ let parsed;
2506
+ try {
2507
+ parsed = parseArgs({
2508
+ args,
2509
+ allowPositionals: false,
2510
+ strict: true,
2511
+ tokens: true,
2512
+ options: {
2513
+ help: { type: 'boolean', short: 'h' },
2514
+ json: { type: 'boolean' },
2515
+ 'timeout-ms': { type: 'string' },
2516
+ },
2517
+ });
2518
+ }
2519
+ catch (error) {
2520
+ throw new CliError(String(error), { code: 'INVALID_ARGS', exitCode: 2 });
2521
+ }
2522
+ const timeoutOccurrences = parsed.tokens.filter((entry) => entry.kind === 'option' && entry.name === 'timeout-ms').length;
2523
+ if (timeoutOccurrences > 1) {
2524
+ throw new CliError('Option --timeout-ms may be provided only once.', {
2525
+ code: 'INVALID_ARGS',
2526
+ exitCode: 2,
2527
+ });
2528
+ }
2529
+ const timeoutText = parsed.values['timeout-ms'];
2530
+ const timeoutMs = typeof timeoutText === 'string' ? Number(timeoutText) : 10_000;
2531
+ if (!Number.isSafeInteger(timeoutMs) ||
2532
+ timeoutMs <= 0 ||
2533
+ timeoutMs > AUTH_IDENTITY_MAX_TIMEOUT_MS) {
2534
+ throw new CliError(`Expected --timeout-ms to be an integer between 1 and ${AUTH_IDENTITY_MAX_TIMEOUT_MS}.`, { code: 'INVALID_TIMEOUT', exitCode: 2 });
2535
+ }
2536
+ return {
2537
+ help: Boolean(parsed.values.help),
2538
+ json: Boolean(parsed.values.json),
2539
+ timeoutMs,
2540
+ };
2541
+ }
2372
2542
  function parseRemoteFlags(args) {
2373
2543
  let values;
2374
2544
  try {
@@ -5543,6 +5713,9 @@ export async function executeCli(argv, deps) {
5543
5713
  try {
5544
5714
  const { flags, command, subcommand, commandArgs } = parseCommandLine(argv);
5545
5715
  const authIdentityReceiptImpl = deps.runAuthIdentityReceiptImpl ?? runAuthIdentityReceipt;
5716
+ const oauthLoginImpl = deps.loginWithSupabaseOAuthImpl ?? loginWithSupabaseOAuth;
5717
+ const oauthLogoutImpl = deps.logoutSupabaseUserSessionImpl ?? logoutSupabaseUserSession;
5718
+ const authStatusImpl = deps.inspectSupabaseAuthStatusImpl ?? inspectSupabaseAuthStatus;
5546
5719
  const publishImpl = deps.runPublishImpl ?? runPublish;
5547
5720
  const validationImpl = deps.runValidationImpl ?? runValidation;
5548
5721
  const lcaReleaseImpl = deps.runLcaReleaseImpl ?? runLcaRelease;
@@ -5671,6 +5844,90 @@ export async function executeCli(argv, deps) {
5671
5844
  stderr: '',
5672
5845
  };
5673
5846
  }
5847
+ if (command === 'auth' && subcommand === 'status') {
5848
+ const authFlags = parseAuthLogoutFlags(commandArgs);
5849
+ if (authFlags.help) {
5850
+ return { exitCode: 0, stdout: `${renderAuthStatusHelp()}\n`, stderr: '' };
5851
+ }
5852
+ const receipt = authStatusImpl({ runtime: requireSupabaseRestRuntime(deps.env) });
5853
+ return {
5854
+ exitCode: receipt.status === 'ready' ? 0 : 1,
5855
+ stdout: stringifyJson(receipt, authFlags.json),
5856
+ stderr: '',
5857
+ };
5858
+ }
5859
+ if (command === 'auth' && subcommand === 'whoami') {
5860
+ const authFlags = parseAuthLiveFlags(commandArgs);
5861
+ if (authFlags.help) {
5862
+ return { exitCode: 0, stdout: `${renderAuthWhoamiHelp()}\n`, stderr: '' };
5863
+ }
5864
+ const receipt = await authIdentityReceiptImpl({
5865
+ env: deps.env,
5866
+ fetchImpl: deps.fetchImpl,
5867
+ cliVersion: loadCliPackageVersion(import.meta.url),
5868
+ expectedProjectRef: null,
5869
+ expectedUserId: null,
5870
+ timeoutMs: authFlags.timeoutMs,
5871
+ });
5872
+ return { exitCode: 0, stdout: stringifyJson(receipt, authFlags.json), stderr: '' };
5873
+ }
5874
+ if (command === 'auth' && subcommand === 'doctor-auth') {
5875
+ const authFlags = parseAuthLiveFlags(commandArgs);
5876
+ if (authFlags.help) {
5877
+ return { exitCode: 0, stdout: `${renderAuthDoctorHelp()}\n`, stderr: '' };
5878
+ }
5879
+ const local = authStatusImpl({ runtime: requireSupabaseRestRuntime(deps.env) });
5880
+ if (local.status !== 'ready') {
5881
+ return {
5882
+ exitCode: 1,
5883
+ stdout: stringifyJson({
5884
+ schemaVersion: 'tiangong.cli-auth-doctor.v1',
5885
+ status: 'login-required',
5886
+ local,
5887
+ live: null,
5888
+ }, authFlags.json),
5889
+ stderr: '',
5890
+ };
5891
+ }
5892
+ const live = await authIdentityReceiptImpl({
5893
+ env: deps.env,
5894
+ fetchImpl: deps.fetchImpl,
5895
+ cliVersion: loadCliPackageVersion(import.meta.url),
5896
+ expectedProjectRef: null,
5897
+ expectedUserId: null,
5898
+ timeoutMs: authFlags.timeoutMs,
5899
+ });
5900
+ return {
5901
+ exitCode: 0,
5902
+ stdout: stringifyJson({
5903
+ schemaVersion: 'tiangong.cli-auth-doctor.v1',
5904
+ status: 'passed',
5905
+ local,
5906
+ live,
5907
+ }, authFlags.json),
5908
+ stderr: '',
5909
+ };
5910
+ }
5911
+ if (command === 'auth' && subcommand === 'login') {
5912
+ const authFlags = parseAuthLoginFlags(commandArgs);
5913
+ if (authFlags.help) {
5914
+ return { exitCode: 0, stdout: `${renderAuthLoginHelp()}\n`, stderr: '' };
5915
+ }
5916
+ const receipt = await oauthLoginImpl({
5917
+ runtime: requireSupabaseRestRuntime(deps.env),
5918
+ fetchImpl: deps.fetchImpl,
5919
+ loginTimeoutMs: authFlags.timeoutMs,
5920
+ });
5921
+ return { exitCode: 0, stdout: stringifyJson(receipt, authFlags.json), stderr: '' };
5922
+ }
5923
+ if (command === 'auth' && subcommand === 'logout') {
5924
+ const authFlags = parseAuthLogoutFlags(commandArgs);
5925
+ if (authFlags.help) {
5926
+ return { exitCode: 0, stdout: `${renderAuthLogoutHelp()}\n`, stderr: '' };
5927
+ }
5928
+ const receipt = await oauthLogoutImpl({ runtime: requireSupabaseRestRuntime(deps.env) });
5929
+ return { exitCode: 0, stdout: stringifyJson(receipt, authFlags.json), stderr: '' };
5930
+ }
5674
5931
  if (command === 'search' && !subcommand && commandArgs.includes('--help')) {
5675
5932
  return { exitCode: 0, stdout: `${renderSearchHelp()}\n`, stderr: '' };
5676
5933
  }