@sequenceholdings/studio-cli 0.1.13 → 0.1.18

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 (53) hide show
  1. package/README.md +94 -30
  2. package/dist/artifact/delegate.d.ts +2 -2
  3. package/dist/artifact/delegate.js +31 -73
  4. package/dist/atlas-client.js +52 -37
  5. package/dist/auth-cmds/commands.d.ts +1 -1
  6. package/dist/auth-cmds/commands.js +12 -7
  7. package/dist/auth.d.ts +97 -19
  8. package/dist/auth.js +376 -81
  9. package/dist/config.d.ts +3 -3
  10. package/dist/config.js +18 -13
  11. package/dist/env-catalog.js +13 -3
  12. package/dist/env-flags.d.ts +2 -0
  13. package/dist/env-flags.js +2 -0
  14. package/dist/env-registry.d.ts +27 -0
  15. package/dist/env-registry.js +204 -0
  16. package/dist/envs/commands.d.ts +1 -1
  17. package/dist/envs/commands.js +41 -3
  18. package/dist/file-lock.d.ts +5 -0
  19. package/dist/file-lock.js +187 -0
  20. package/dist/functions/commands.d.ts +9 -1
  21. package/dist/functions/commands.js +71 -29
  22. package/dist/functions/manifest.d.ts +1 -0
  23. package/dist/functions/manifest.js +36 -0
  24. package/dist/login.d.ts +8 -3
  25. package/dist/login.js +46 -34
  26. package/dist/main.d.ts +2 -1
  27. package/dist/main.js +35 -12
  28. package/dist/orm/delegate.js +15 -2
  29. package/dist/pat-hints.js +2 -2
  30. package/dist/pipeline/commands.d.ts +58 -0
  31. package/dist/pipeline/commands.js +330 -0
  32. package/dist/pipeline/lifecycle.d.ts +58 -0
  33. package/dist/pipeline/lifecycle.js +348 -0
  34. package/dist/pipeline/pinning.d.ts +5 -0
  35. package/dist/pipeline/pinning.js +9 -0
  36. package/dist/pipeline/templates.d.ts +11 -0
  37. package/dist/pipeline/templates.js +166 -0
  38. package/dist/process/build.d.ts +4 -0
  39. package/dist/process/build.js +31 -1
  40. package/dist/process/codegen.js +19 -1
  41. package/dist/process/commands.js +97 -47
  42. package/dist/process/compiler-subprocess.d.ts +29 -0
  43. package/dist/process/compiler-subprocess.js +99 -0
  44. package/dist/process/compiler-worker.d.ts +1 -0
  45. package/dist/process/compiler-worker.js +38 -0
  46. package/dist/process/lint.d.ts +8 -0
  47. package/dist/process/lint.js +76 -29
  48. package/dist/process/repo-install.js +18 -2
  49. package/dist/repos/commands.d.ts +1 -1
  50. package/dist/repos/commands.js +17 -12
  51. package/dist/secrets/commands.d.ts +1 -1
  52. package/dist/secrets/commands.js +18 -18
  53. package/package.json +8 -3
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # `@sequenceholdings/studio-cli` — `seq-studio`
2
2
 
3
3
  Standalone CLI for the Sequence platform: Lattice processes, Artifact Studio
4
- apps, Managed Functions, Managed Secrets, and platform git repos. Runs from
4
+ apps, Managed Functions, Managed Secrets, Data Pipelines stage specs, and platform git repos. Runs from
5
5
  any repo against the platform over HTTP — no monorepo checkout required.
6
6
 
7
7
  ```
@@ -9,13 +9,15 @@ seq-studio process lint
9
9
  seq-studio process plan -e <env>
10
10
  seq-studio process apply -e <env>
11
11
  seq-studio artifact deploy -e <env>
12
+ seq-studio pipeline validate
12
13
  seq-studio envs list
13
14
  seq-studio doctor
14
15
  ```
15
16
 
16
17
  > **Note:** all network commands require a Sequence platform account with the
17
18
  > appropriate permissions. Without one, only the offline commands
18
- > (`init`, `lint`, `simulate`, `build`, `bundle inspect`) work.
19
+ > (`init`, `lint`, `simulate`, `build`, `bundle inspect`, `pipeline init`,
20
+ > `pipeline validate`) work.
19
21
  > Sequence-internal contributors: see `INTERNAL.md` in the monorepo for
20
22
  > rollout SOPs, preview environments, and publishing docs.
21
23
 
@@ -44,16 +46,20 @@ releases install immediately.)
44
46
 
45
47
  ## Authenticate
46
48
 
47
- Run the built-in browser login once. `seq-studio` and `seqapi` share the
48
- resulting tokens at `~/.config/sequence-api/tokens.json`, so logging in with
49
- either CLI authenticates both.
49
+ Run the built-in browser login. `seq-studio` and `seqapi` share short-lived
50
+ access tokens at `~/.config/sequence-api/tokens.json`, so logging in with either
51
+ CLI authenticates both. Refresh tokens are neither requested nor persisted;
52
+ interactive commands perform a bounded PKCE login again after expiry.
50
53
 
51
54
  ```bash
52
- seq-studio login
53
- seq-studio doctor # confirms config + auth + authorization
55
+ seq-studio login # built-in Sequence environments
56
+ seq-studio envs add bsm-staging <tenant-url> # one-time tenant registration
57
+ seq-studio login --env bsm-staging # registered OpCo tenant realm
58
+ seq-studio doctor # confirms config + auth + authorization
54
59
  ```
55
60
 
56
- Use `seq-studio logout` to remove the shared cached tokens.
61
+ Use `seq-studio logout` to remove the shared Sequence session, or
62
+ `seq-studio logout --env bsm-staging` to remove only that OpCo realm's session.
57
63
 
58
64
  ### Headless auth (CI) — M2M
59
65
 
@@ -62,16 +68,19 @@ secret and `seq-studio` mints a token via the Auth0 client-credentials grant
62
68
  (M2M wins when the secret is set, otherwise the cached user token is used):
63
69
 
64
70
  ```bash
65
- export AUTH0_M2M_CLIENT_SECRET=... # provided by your platform administrator
71
+ export AUTH0_M2M_CLIENT_SECRET=... # built-in Sequence environments
72
+ export AUTH0_M2M_CLIENT_SECRET_BSM_STAGING=... # registered bsm-staging realm
66
73
  seq-studio artifact deploy -e <env>
67
74
  ```
68
75
 
69
- The secret is read at runtime never commit it. M2M carries app scopes but
76
+ Registered OpCo environment names are upper-snaked in the variable suffix
77
+ (`bsm-staging` → `BSM_STAGING`). Secrets are read at runtime — never commit them.
78
+ M2M carries app access but
70
79
  **no user identity / workspace membership**, so it's blind to
71
80
  user-scoped/private resources.
72
81
 
73
- **Manual escape hatch:** any `artifact` command also accepts an explicit
74
- `--token <jwt>`, which wins over both the M2M and cached-user paths.
82
+ Artifact commands do not accept bearer tokens through argv or environment.
83
+ Use interactive login or the realm-specific M2M secret above.
75
84
 
76
85
  ## Environments
77
86
 
@@ -82,7 +91,7 @@ environment-discovery endpoint and caches the result at
82
91
  `~/.config/lattice/environments.json`.
83
92
 
84
93
  ```bash
85
- seq-studio login # or: export AUTH0_M2M_CLIENT_SECRET=...
94
+ seq-studio login # or use the matching M2M secret described above
86
95
  seq-studio envs refresh # fetch the environments visible to your identity
87
96
  seq-studio envs list # show them (name, URL, source)
88
97
  ```
@@ -93,8 +102,12 @@ see depends on who you are — unauthenticated installs get `local` only, and
93
102
  authenticated identities get the deployments they're entitled to. Visibility
94
103
  is not access control: every request is still authorized server-side.
95
104
 
96
- You can always add or override environments yourself in
97
- `~/.config/lattice/config.toml` (user entries win over discovered ones):
105
+ You can add or override trusted environments yourself in
106
+ `~/.config/lattice/config.toml` (user entries win over discovered ones).
107
+ Authenticated requests accept only HTTPS origins at `seqholdings.com` or its
108
+ subdomains, plus HTTP loopback origins for local development. The CLI
109
+ revalidates that exact origin immediately before attaching credentials and
110
+ never follows authenticated redirects.
98
111
 
99
112
  ```toml
100
113
  # Must come before any [env.*] table — TOML attaches bare keys to the
@@ -105,7 +118,7 @@ default_env = "local"
105
118
  url = "http://localhost:5001"
106
119
 
107
120
  [env.my-atlas]
108
- url = "https://atlas.example.com"
121
+ url = "https://my-atlas.seqholdings.com"
109
122
  ```
110
123
 
111
124
  Pass `--env <name>` (or `-e <name>`) on commands that talk to the platform.
@@ -123,9 +136,9 @@ the artifact folder's `.artifact-studio/config.json` `defaultEnv` (set by
123
136
  | `seq-studio process lint` | Static checks (graph, return contracts, agent schema, timeouts) |
124
137
  | `seq-studio process plan -e <env>` | Build bundle, diff against currently-active version |
125
138
  | `seq-studio process apply -e <env> [--only <id1,id2>]` | Build → register bundle → promote each process. `--only` promotes just the named process ids (the bundle still contains the whole root — registration is inert) |
126
- | `seq-studio process apply --repo processes/<name> [--ref <r>] -e <env> [--only <id1,id2>]` | Same as above, but materializes the source from a platform git-service repo. Pinned commit SHA is injected as bundle provenance. Requires `ATLAS_GIT_PAT` (or `AUTH0_M2M_CLIENT_SECRET` for CI) |
139
+ | `seq-studio process apply --repo processes/<name> [--ref <r>] -e <env> [--only <id1,id2>]` | Same as above, but materializes the source from a platform git-service repo. Pinned commit SHA is injected as bundle provenance. Requires `ATLAS_GIT_PAT` (or the target realm's M2M secret for CI) |
127
140
  | `seq-studio process test -e <env>` | CI wrapper: lint + plan, non-zero exit on errors or BREAKING diffs |
128
- | `seq-studio process simulate <id>` | In-process walk with stubbed runners (offline) |
141
+ | `seq-studio process simulate <id>` | Isolated child-process walk with stubbed runners (offline) |
129
142
  | `seq-studio process bundle build [-o file.json]` | Build a bundle locally |
130
143
  | `seq-studio process bundle pull <hash> [-e <env>] [-o file.json]` | Fetch a stored bundle |
131
144
  | `seq-studio process bundle inspect <bundle.json>` | Show a saved bundle's summary |
@@ -149,7 +162,16 @@ temp tree is cleaned up — even on error. Only the `processes` namespace is
149
162
  accepted; other namespaces (artifacts, managed-functions) are rejected.
150
163
 
151
164
  Auth: same as `artifact deploy --repo` — `ATLAS_GIT_PAT` for the smart-HTTP
152
- clone path, or `AUTH0_M2M_CLIENT_SECRET` for CI's JSON materialize path.
165
+ clone path, or the target realm's M2M secret for CI's JSON materialize path.
166
+ Those credentials remain in the parent CLI and are not forwarded to dependency
167
+ installation or process build tooling. Each install uses fresh temporary
168
+ package-manager caches that are removed after the invocation.
169
+
170
+ This is ambient-credential isolation, not an OS security sandbox. Build workers
171
+ still run as the caller's uid, so hostile source may inspect other same-uid
172
+ processes or readable files on platforms that permit it (for example, Linux
173
+ `/proc`). Build reviewed source only; use a dedicated ephemeral runner with no
174
+ unrelated credentials when the source is not trusted.
153
175
 
154
176
  ### Process discovery
155
177
 
@@ -206,14 +228,26 @@ of the pipeline (build → upload) is identical regardless of origin:
206
228
  |------|--------|
207
229
  | _(none)_ / `[dir]` | a local folder (default, unchanged) |
208
230
  | `--repo <ns>/<name>` | a repo in the platform Git Service, e.g. `--repo artifacts/accounts` |
209
- | `--git-url <url>` | any git URL (cloned to a temp dir) |
231
+ | `--git-url <url>` | a public, credential-free HTTPS git URL (cloned to a temp dir) |
210
232
 
211
233
  `--ref <branch\|tag\|commit>` selects the revision (default: the repo's default
212
234
  branch). A remote source is materialized to a temp dir, built, and torn down;
213
235
  the target project is resolved from the source's `artifact.bundle.yml`
214
236
  `project_id`. The deployment records the resolved commit as its `git_commit`
215
- provenance. `--repo` needs `-e <env>` + a platform login; `--git-url` needs git
216
- credentials for private repos. `dev` is local-only.
237
+ provenance. `--repo` needs `-e <env>` + a platform login. M2M/CI builds accept
238
+ only platform-managed `--repo` sources; `--git-url` is interactive-only and
239
+ rejects embedded credentials, query tokens, SSH/file URLs, and Git remote
240
+ helpers. `dev` is local-only.
241
+
242
+ Artifact builds, dependency installs, process-definition imports, and local
243
+ process simulations run in short-lived children with an allowlisted runtime
244
+ environment and a synthetic home/config directory. Parent bearer tokens, Auth0
245
+ M2M secrets, Doppler tokens, cloud credentials, GitHub tokens, and user config
246
+ files are not inherited. The platform Git Service clone path passes only its
247
+ single scoped PAT to `git` through an ephemeral askpass helper.
248
+ This is a credential-inheritance boundary, not an OS filesystem sandbox:
249
+ child code still runs as the invoking user and must be treated as trusted to
250
+ read files that it names explicitly outside the synthetic home.
217
251
 
218
252
  ## Pull / clone / deploy — which path?
219
253
 
@@ -252,7 +286,7 @@ You can also open **Repositories → Access tokens** / the clone popover’s
252
286
  Requires Auth0 login. Same identity as the UI:
253
287
 
254
288
  ```bash
255
- seq-studio login
289
+ seq-studio login # add --env <registered-opco-env> for a tenant realm
256
290
  seq-studio auth pat create --name laptop --scopes repo:read,repo:write -e <env>
257
291
  # optional: --expires 7d|30d|90d|1y|never (default 30d)
258
292
  # optional: --store-credentials # git credential approve for the env host
@@ -270,18 +304,48 @@ JSON API — the same repos `--repo <ns>/<name>` sources build from.
270
304
 
271
305
  | Command | What it does |
272
306
  |---------|--------------|
273
- | `seq-studio repos list [-e <env>] [--namespace <slug>] [--mine]` | repos visible on the environment (permission-filtered) |
274
- | `seq-studio repos namespaces [create <slug>] [-e <env>]` | list namespaces, or create one (creator becomes owner) |
275
- | `seq-studio repos show <ns>/<name> [-e <env>]` | detail: id, branches, clone URL; artifact project id when slug matches |
276
- | `seq-studio repos create <ns>/<name> [-e <env>] [--default-branch <b>]` | create an empty repo (needs namespace write) |
277
- | `seq-studio repos clone <ns>/<name> \| --url <clone-url> \| --id <uuid> [-e <env>] [--ref <r>] [--out <dir>] [--force]` | smart-HTTP `git clone` when `ATLAS_GIT_PAT` is set (`--url`/`--id` need no seqapi); otherwise JSON materialize + PAT hint |
278
- | `seq-studio repos pull <ns>/<name> [-e <env>] [--ref <r>] [--out <dir>] [--force]` | always materialize via JSON API (no `.git` dir); refuses a non-empty destination unless `--force` |
279
- | `seq-studio repos delete <ns>/<name> [-e <env>] [--yes]` | delete a repo — interactive confirm unless `--yes` |
307
+ | `seq-studio repos list -e <env> [--namespace <slug>] [--mine]` | repos visible on the environment (permission-filtered) |
308
+ | `seq-studio repos namespaces [create <slug>] -e <env>` | list namespaces, or create one (creator becomes owner) |
309
+ | `seq-studio repos show <ns>/<name> -e <env>` | detail: id, branches, clone URL; artifact project id when slug matches |
310
+ | `seq-studio repos create <ns>/<name> -e <env> [--default-branch <b>]` | create an empty repo (needs namespace write) |
311
+ | `seq-studio repos clone <ns>/<name> \| --url <clone-url> \| --id <uuid> -e <env> [--ref <r>] [--out <dir>] [--force]` | smart-HTTP `git clone` when `ATLAS_GIT_PAT` is set (`--url`/`--id` need no seqapi); otherwise JSON materialize + PAT hint |
312
+ | `seq-studio repos pull <ns>/<name> -e <env> [--ref <r>] [--out <dir>] [--force]` | always materialize via JSON API (no `.git` dir); refuses a non-empty destination unless `--force` |
313
+ | `seq-studio repos delete <ns>/<name> -e <env> [--yes]` | delete a repo — interactive confirm unless `--yes` |
280
314
 
281
315
  `show` prints the smart-HTTP clone URL (`…/repos/<id>/git`). Basic auth:
282
316
  any username, PAT as password. Prefer `repos clone` over hand-rolling the
283
317
  tree API.
284
318
 
319
+ ## Pipeline commands
320
+
321
+ `seq-studio pipeline <sub>` authors and validates Data Pipelines **stage
322
+ specs** — the typed contracts (`<name>.stage.yml`) a Pipeline repo
323
+ (`pipelines/<domain>`) is made of. Validation logic lives in
324
+ `@sequenceholdings/pipeline-spec` (an optional peer, like `@sequenceholdings/orm`);
325
+ install it alongside the CLI to use this family.
326
+
327
+ | Command | What it does |
328
+ |---------|--------------|
329
+ | `seq-studio pipeline init --type ingestion\|transformation\|serving <name> [--dir <dir>]` | Scaffold `<name>.stage.yml` (commented per-kind template) plus a `src/` entrypoint stub (serving stages are declarative — no stub). Refuses to overwrite an existing spec |
330
+ | `seq-studio pipeline validate [dir] [--assets <file\|url>] [--json]` | Run the full offline spec gate: envelope + body validation, `schema_ref` resolution, and repo-level graph validation (reference resolution, single-writer, cycles, column subsets, serving projection checks). Exit 0/1 |
331
+ | `seq-studio pipeline plan --repo pipelines/<slug> --ref <sha\|branch> -e <env> [--json]` | Plan a Pipeline deploy (materialize → SDK/`validateSpecGraph` → compile → live-diff → provision findings). Does **not** run Databricks `bundle validate` (that is a Trigger deploy-path hard gate). Exit 1 on destructive findings (CI-safe). `--json` emits the stable plan envelope |
332
+ | `seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha> -e <env> [--approved-by <sub>] [--no-wait]` | Plan then enqueue deploy; Trigger runs `bundle validate` then `bundle deploy` against reviewed bytes. Polls to terminal unless `--no-wait`. Production/banksouth require a pinned 40-hex SHA (client + server) |
333
+ | `seq-studio pipeline run-now --stage <slug> -e <env> [--repo pipelines/<slug>] [--json]` | Run the stage's active job or DLT pipeline immediately and print its Databricks run URL |
334
+ | `seq-studio pipeline promote --stage <slug> --version <v> -e <env> [--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]` | Promote a validated version to another environment. Prod/banksouth require `--approved-by` naming the authenticated caller (approvals are self-recorded); `--repo` disambiguates a slug that exists in multiple Pipelines |
335
+ | `seq-studio pipeline rollback --stage <slug> -e <env> [--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]` | Redeploy the previously retired deployment's version. Prod/banksouth require `--approved-by` — approvals are explicit even for rollbacks |
336
+
337
+ `validate` is offline — no network or database. `--assets` supplies a
338
+ registry **asset export** (JSON) so inputs referencing other Pipelines'
339
+ assets resolve; pass a local file or an authenticated URL. `--json` emits a
340
+ stable machine shape (`{ ok, stages, findings[] }` with machine-readable
341
+ finding codes and consumer lists) for CI annotation — the
342
+ `pipeline-verify.yml` workflow runs exactly this on PRs touching
343
+ `pipelines/**`.
344
+
345
+ For local Atlas development, `-e local` uses the server's isolated `dev`
346
+ deployment target. Plan, deploy, and `run-now` therefore operate on the same
347
+ personal dev Databricks catalog and Lakebase branch configured for local Atlas.
348
+
285
349
  ## Authorization
286
350
 
287
351
  The CLI carries no permissions of its own: every request is authorized
@@ -7,8 +7,8 @@
7
7
  * 1. Resolve `--env` from `~/.config/lattice/config.toml`.
8
8
  * 2. Set `ARTIFACT_STUDIO_BASE_URL` so artifact-studio's API client
9
9
  * uses the resolved URL (instead of its own built-in env map).
10
- * 3. Set `ARTIFACT_STUDIO_TOKEN` from the seqapi token file so auth
11
- * is shared with the rest of `seq-studio`.
10
+ * 3. Install an internal token provider so auth is shared with the rest of
11
+ * `seq-studio` without exposing the bearer in argv or environment.
12
12
  * 4. Forward all remaining argv to `runCli`.
13
13
  */
14
14
  export declare function runArtifactCommand(sub: string | undefined, rest: string[]): Promise<number>;
@@ -7,11 +7,11 @@
7
7
  * 1. Resolve `--env` from `~/.config/lattice/config.toml`.
8
8
  * 2. Set `ARTIFACT_STUDIO_BASE_URL` so artifact-studio's API client
9
9
  * uses the resolved URL (instead of its own built-in env map).
10
- * 3. Set `ARTIFACT_STUDIO_TOKEN` from the seqapi token file so auth
11
- * is shared with the rest of `seq-studio`.
10
+ * 3. Install an internal token provider so auth is shared with the rest of
11
+ * `seq-studio` without exposing the bearer in argv or environment.
12
12
  * 4. Forward all remaining argv to `runCli`.
13
13
  */
14
- import { getAccessToken, M2mTokenError, tryGetAccessToken } from '../auth.js';
14
+ import { getAccessTokenWithMode, M2mTokenError, tryGetAccessTokenWithMode, } from '../auth.js';
15
15
  import { readConfig, resolveEnvWithDiscovery } from '../config.js';
16
16
  import { fetchCatalog, readCachedCatalog } from '../env-catalog.js';
17
17
  import { normalizeShortEnvFlag, readEnvFromArgv } from '../env-flags.js';
@@ -36,11 +36,13 @@ const ARTIFACT_USAGE = `usage:
36
36
  discovered after you authenticate).
37
37
 
38
38
  Source for build/plan/deploy: a local [dir] (default), a platform git-service
39
- repo (--repo <ns>/<name>), or any git URL (--git-url <url>). --ref selects a
39
+ repo (--repo <ns>/<name>), or a public HTTPS git URL (--git-url <url>). --ref selects a
40
40
  branch/tag/commit (default: the repo's default branch).
41
41
 
42
- --repo clones over smart-HTTP and requires ATLAS_GIT_PAT (a repo:read PAT —
43
- seq-studio auth pat create, or Atlas → Settings → Tokens).
42
+ Interactive --repo builds clone over smart-HTTP and require ATLAS_GIT_PAT
43
+ (a repo:read PAT — seq-studio auth pat create, or Atlas → Settings → Tokens).
44
+ Headless M2M builds use JSON materialize and accept only platform-managed
45
+ --repo sources.
44
46
 
45
47
  Per-PR preview environments (https://studio-atlas-git-<slug>.preview.seqholdings.com):
46
48
  -e preview:<branch-or-slug> compute the preview host from a branch name
@@ -53,23 +55,20 @@ const ARTIFACT_USAGE = `usage:
53
55
  Authenticate with: seq-studio login
54
56
  `;
55
57
  export async function runArtifactCommand(sub, rest) {
58
+ if (rest.some((arg) => arg === '--token' || arg.startsWith('--token='))) {
59
+ console.error('seq-studio artifact does not accept bearer tokens via --token. ' +
60
+ 'Use `seq-studio login` interactively or configure the realm-specific ' +
61
+ 'AUTH0_M2M_CLIENT_SECRET for headless automation.');
62
+ return 1;
63
+ }
64
+ // Never pass a legacy bearer environment variable into the embedded package.
65
+ delete process.env['ARTIFACT_STUDIO_TOKEN'];
56
66
  // Older artifact-studio versions exposed nested login/logout commands backed
57
67
  // by a separate token file. Keep those commands working, but route them to
58
68
  // seq-studio's shared token so every namespace uses the same identity.
59
69
  if (sub === 'login' || sub === 'logout') {
60
70
  if (rest.length > 0) {
61
- if (sub === 'login' && rest.includes('--token')) {
62
- // The legacy `artifact login --token <jwt>` persisted a bearer to
63
- // artifact-studio's own token file — a store this unification retires.
64
- console.error('seq-studio artifact login no longer stores a bearer token.\n' +
65
- 'Scripted/headless options:\n' +
66
- ' - pass --token <jwt> directly to the artifact command (deploy/plan/whoami/...)\n' +
67
- ' - export ARTIFACT_STUDIO_TOKEN=<jwt> for the session\n' +
68
- ' - set AUTH0_M2M_CLIENT_SECRET for service-account (M2M) auth in CI');
69
- }
70
- else {
71
- console.error(`seq-studio artifact ${sub} does not accept arguments.`);
72
- }
71
+ console.error(`seq-studio artifact ${sub} does not accept arguments.`);
73
72
  return 1;
74
73
  }
75
74
  const auth = await import('../login.js');
@@ -133,51 +132,22 @@ export async function runArtifactCommand(sub, rest) {
133
132
  // without an explicit --env flag — the stored `.artifact-studio/config.json`
134
133
  // defaultEnv, `artifact env set <name>` — would otherwise fall back to
135
134
  // artifact-studio's built-in local-only map and fail on deployed envs.
136
- const { envs } = await readConfig();
137
- process.env['ARTIFACT_STUDIO_ENV_URLS'] = JSON.stringify(Object.fromEntries(Object.entries(envs).map(([name, { url }]) => [name, url])));
138
- // An explicit `--token <jwt>` is the manual escape hatch and must win over
139
- // everything, including a configured-but-failing M2M credential (which
140
- // `tryGetAccessToken({ failClosedForM2m: true })` would otherwise turn into
141
- // an abort before argv ever reaches artifact-studio). artifact-studio's
142
- // `getOptionalToken` checks `flags.token` first, so when it's present we
143
- // skip shared-token resolution entirely. Only the two-token `--token <jwt>`
144
- // form counts: artifact-studio's parser does not split `--token=<jwt>`.
145
- const hasExplicitToken = hasTokenFlag(argvForCli);
135
+ const routingConfig = await readConfig();
136
+ process.env['ARTIFACT_STUDIO_ENV_URLS'] = JSON.stringify(Object.fromEntries(Object.entries(routingConfig.envs).map(([name, { url }]) => [name, url])));
146
137
  // Lazy import so `process` / `doctor` commands don't pull in
147
138
  // artifact-studio's vite/react/tailwind dependency graph.
148
- const { runCli: runArtifactStudio, setTokenProvider } = await import('@sequenceholdings/artifact-studio/cli');
149
- if (!hasExplicitToken) {
150
- // Share the seqapi token. `tryGetAccessToken` resolves an M2M
151
- // service-account token when AUTH0_M2M_CLIENT_SECRET is set (headless /
152
- // CI / cloud-agent path) and otherwise the cached interactive user
153
- // token (see ../auth.ts). If neither is available, artifact-studio
154
- // commands that need a token surface their own error — we don't force
155
- // `seq-studio login` here because some commands (init, validate, build)
156
- // work offline.
157
- const token = await tryGetAccessToken({ failClosedForM2m: true });
158
- if (token) {
159
- process.env['ARTIFACT_STUDIO_TOKEN'] = token;
160
- }
161
- // The ARTIFACT_STUDIO_TOKEN env var above is captured once and never
162
- // refreshes, so long-running commands (notably `artifact dev`) would start
163
- // failing with "Authentication failed" once the initial token's TTL
164
- // elapses. Hand artifact-studio a refreshing source — getAccessToken()
165
- // mints a fresh access token via the Auth0 refresh grant when the cached
166
- // one is near expiry — so a watch session survives indefinitely.
167
- setTokenProvider(async () => {
168
- if (process.env.AUTH0_M2M_CLIENT_SECRET?.trim()) {
169
- // Fail closed for configured M2M failures so headless deploys never
170
- // silently fall back to another cached identity.
171
- return await getAccessToken();
172
- }
173
- try {
174
- return await getAccessToken();
175
- }
176
- catch {
177
- return null;
178
- }
179
- });
180
- }
139
+ const { getConfiguredDefaultEnv, runCli: runArtifactStudio, setTokenProvider } = await import('@sequenceholdings/artifact-studio/cli');
140
+ const authEnvName = resolved?.name ?? (await getConfiguredDefaultEnv());
141
+ const targetUrl = resolved?.url ??
142
+ process.env['ARTIFACT_STUDIO_BASE_URL']?.trim() ??
143
+ (authEnvName ? routingConfig.envs[authEnvName]?.url : undefined);
144
+ setTokenProvider(({ allowInteractiveLogin }) => allowInteractiveLogin
145
+ ? getAccessTokenWithMode({ env: authEnvName, targetUrl })
146
+ : tryGetAccessTokenWithMode({
147
+ failClosedForM2m: true,
148
+ env: authEnvName,
149
+ targetUrl,
150
+ }));
181
151
  return runArtifactStudio([sub, ...argvForCli]);
182
152
  }
183
153
  /**
@@ -240,18 +210,6 @@ export function extractPreviewFlags(argv) {
240
210
  }
241
211
  return { rest, prNumber, envUrl };
242
212
  }
243
- /**
244
- * True when argv carries an explicit two-token `--token <jwt>` that
245
- * artifact-studio's parser will bind to `flags.token`. `--token=<jwt>` is
246
- * excluded because that parser stores it as a stray flag and never binds it.
247
- */
248
- function hasTokenFlag(argv) {
249
- const index = argv.indexOf('--token');
250
- if (index === -1)
251
- return false;
252
- const value = argv[index + 1];
253
- return value !== undefined && !value.startsWith('--');
254
- }
255
213
  function splitInlineValue(arg) {
256
214
  const eq = arg.indexOf('=');
257
215
  if (arg.startsWith('--') && eq !== -1)
@@ -4,6 +4,7 @@
4
4
  * consistent across both Sequence CLIs. Token + baseUrl are passed in by
5
5
  * the caller — this file knows nothing about token files or config TOMLs.
6
6
  */
7
+ import { authenticatedRequestUrl } from '@sequenceholdings/artifact-studio/deployment-validation';
7
8
  import { PREVIEW_DOMAIN } from './preview.js';
8
9
  const MAX_503_RETRIES = 5;
9
10
  const DEFAULT_RETRY_AFTER_SECONDS = 2;
@@ -34,6 +35,18 @@ async function fetchWith503Retry(input, init) {
34
35
  await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
35
36
  }
36
37
  }
38
+ async function authenticatedFetch({ baseUrl, init = {}, path, token, }) {
39
+ const url = authenticatedRequestUrl({ baseUrl, path });
40
+ return fetchWith503Retry(url, {
41
+ ...init,
42
+ redirect: 'manual',
43
+ headers: {
44
+ ...previewAccessHeaders(baseUrl),
45
+ ...init.headers,
46
+ Authorization: `Bearer ${token}`,
47
+ },
48
+ });
49
+ }
37
50
  function previewAccessHeaders(baseUrl) {
38
51
  const secret = process.env.PREVIEW_ACCESS_HEADER?.trim();
39
52
  if (!secret)
@@ -89,17 +102,13 @@ async function responseError(response, path) {
89
102
  return new AtlasApiError(response.status, message, path, response.statusText, body);
90
103
  }
91
104
  export async function getJson({ baseUrl, token, path, }) {
92
- const response = await fetchWith503Retry(`${baseUrl}${path}`, {
93
- headers: { ...previewAccessHeaders(baseUrl), Authorization: `Bearer ${token}` },
94
- });
105
+ const response = await authenticatedFetch({ baseUrl, token, path });
95
106
  if (!response.ok)
96
107
  throw await responseError(response, path);
97
108
  return response.json();
98
109
  }
99
110
  export async function getJsonOr404({ baseUrl, token, path, }) {
100
- const response = await fetchWith503Retry(`${baseUrl}${path}`, {
101
- headers: { ...previewAccessHeaders(baseUrl), Authorization: `Bearer ${token}` },
102
- });
111
+ const response = await authenticatedFetch({ baseUrl, token, path });
103
112
  if (response.status === 404)
104
113
  return null;
105
114
  if (!response.ok)
@@ -107,56 +116,60 @@ export async function getJsonOr404({ baseUrl, token, path, }) {
107
116
  return response.json();
108
117
  }
109
118
  export async function postJson({ baseUrl, token, path, body, }) {
110
- const response = await fetchWith503Retry(`${baseUrl}${path}`, {
111
- method: 'POST',
112
- headers: {
113
- ...previewAccessHeaders(baseUrl),
114
- Authorization: `Bearer ${token}`,
115
- 'Content-Type': 'application/json',
119
+ const response = await authenticatedFetch({
120
+ baseUrl,
121
+ token,
122
+ path,
123
+ init: {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json' },
126
+ body: body === undefined ? undefined : JSON.stringify(body),
116
127
  },
117
- body: body === undefined ? undefined : JSON.stringify(body),
118
128
  });
119
129
  if (!response.ok)
120
130
  throw await responseError(response, path);
121
131
  return response.json();
122
132
  }
123
133
  export async function putJson({ baseUrl, token, path, body, }) {
124
- const response = await fetchWith503Retry(`${baseUrl}${path}`, {
125
- method: 'PUT',
126
- headers: {
127
- ...previewAccessHeaders(baseUrl),
128
- Authorization: `Bearer ${token}`,
129
- 'Content-Type': 'application/json',
134
+ const response = await authenticatedFetch({
135
+ baseUrl,
136
+ token,
137
+ path,
138
+ init: {
139
+ method: 'PUT',
140
+ headers: { 'Content-Type': 'application/json' },
141
+ body: body === undefined ? undefined : JSON.stringify(body),
130
142
  },
131
- body: body === undefined ? undefined : JSON.stringify(body),
132
143
  });
133
144
  if (!response.ok)
134
145
  throw await responseError(response, path);
135
146
  return response.json();
136
147
  }
137
148
  export async function patchJson({ baseUrl, token, path, body, }) {
138
- const response = await fetchWith503Retry(`${baseUrl}${path}`, {
139
- method: 'PATCH',
140
- headers: {
141
- ...previewAccessHeaders(baseUrl),
142
- Authorization: `Bearer ${token}`,
143
- 'Content-Type': 'application/json',
149
+ const response = await authenticatedFetch({
150
+ baseUrl,
151
+ token,
152
+ path,
153
+ init: {
154
+ method: 'PATCH',
155
+ headers: { 'Content-Type': 'application/json' },
156
+ body: body === undefined ? undefined : JSON.stringify(body),
144
157
  },
145
- body: body === undefined ? undefined : JSON.stringify(body),
146
158
  });
147
159
  if (!response.ok)
148
160
  throw await responseError(response, path);
149
161
  return response.json();
150
162
  }
151
163
  export async function deleteJson({ baseUrl, token, path, body, }) {
152
- const response = await fetchWith503Retry(`${baseUrl}${path}`, {
153
- method: 'DELETE',
154
- headers: {
155
- ...previewAccessHeaders(baseUrl),
156
- Authorization: `Bearer ${token}`,
157
- 'Content-Type': 'application/json',
164
+ const response = await authenticatedFetch({
165
+ baseUrl,
166
+ token,
167
+ path,
168
+ init: {
169
+ method: 'DELETE',
170
+ headers: { 'Content-Type': 'application/json' },
171
+ body: body === undefined ? undefined : JSON.stringify(body),
158
172
  },
159
- body: body === undefined ? undefined : JSON.stringify(body),
160
173
  });
161
174
  if (!response.ok)
162
175
  throw await responseError(response, path);
@@ -164,9 +177,11 @@ export async function deleteJson({ baseUrl, token, path, body, }) {
164
177
  }
165
178
  /** DELETE for endpoints that return 204 with an empty body (deleteJson would choke on it). */
166
179
  export async function deleteNoContent({ baseUrl, token, path, }) {
167
- const response = await fetchWith503Retry(`${baseUrl}${path}`, {
168
- method: 'DELETE',
169
- headers: { ...previewAccessHeaders(baseUrl), Authorization: `Bearer ${token}` },
180
+ const response = await authenticatedFetch({
181
+ baseUrl,
182
+ token,
183
+ path,
184
+ init: { method: 'DELETE' },
170
185
  });
171
186
  if (!response.ok)
172
187
  throw await responseError(response, path);
@@ -1,7 +1,7 @@
1
1
  import type { ParsedArgs } from '../process/commands.js';
2
2
  declare const PAT_SCOPES: readonly ["repo:read", "repo:write", "repo:admin"];
3
3
  type PatScope = (typeof PAT_SCOPES)[number];
4
- export declare const AUTH_USAGE = "usage:\n seq-studio auth pat create --name <n> [--scopes repo:read,repo:write] [-e env]\n [--expires 7d|30d|90d|1y|never] [--store-credentials]\n seq-studio auth pat list [-e env]\n seq-studio auth pat revoke <id> [-e env] [--yes]\n\n Issue a personal access token for git clone / git push against the platform\n git service.\n\n Authenticate with `seq-studio login`, then run `auth pat create`.\n Alternatively, open Atlas \u2192 Settings \u2192 Tokens:\n https://<atlas-host>/settings/tokens\n Sign in, create a token (repo:read / repo:write), copy once, then:\n export ATLAS_GIT_PAT=<token>\n\n On create the raw token is printed ONCE \u2014 store it; Atlas cannot re-show it.\n Git Basic auth: any username (e.g. git), PAT as the password.\n\n Flags: -e/--env <env> (see: seq-studio envs list)\n";
4
+ export declare const AUTH_USAGE = "usage:\n seq-studio auth pat create --name <n> -e <env> [--scopes repo:read,repo:write]\n [--expires 7d|30d|90d|1y|never] [--store-credentials]\n seq-studio auth pat list -e <env>\n seq-studio auth pat revoke <id> -e <env> [--yes]\n\n Issue a personal access token for git clone / git push against the platform\n git service.\n\n Authenticate with `seq-studio login`, then run `auth pat create -e <env>`.\n Alternatively, open Atlas \u2192 Settings \u2192 Tokens:\n https://<atlas-host>/settings/tokens\n Sign in, create a token (repo:read / repo:write), copy once, then:\n export ATLAS_GIT_PAT=<token>\n\n On create the raw token is printed ONCE \u2014 store it; Atlas cannot re-show it.\n Git Basic auth: any username (e.g. git), PAT as the password.\n\n Flags: -e/--env <env> (required; see: seq-studio envs list)\n";
5
5
  export declare function parsePatScopes(raw: string | undefined): PatScope[];
6
6
  /**
7
7
  * Map UI-style duration choices to an absolute ISO-8601 expiresAt, or undefined
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { deleteNoContent, getJson, postJson } from '../atlas-client.js';
12
12
  import { printCliError } from '../cli-errors.js';
13
+ import { REQUIRE_EXPLICIT_ENV_MESSAGE } from '../env-flags.js';
13
14
  import { buildContext, clientOptions, flagBool, LOG, } from '../functions/commands.js';
14
15
  import { readConfig } from '../config.js';
15
16
  import { confirmYes } from '../prompt.js';
@@ -38,15 +39,15 @@ function stringFlag(flags, key) {
38
39
  return value;
39
40
  }
40
41
  export const AUTH_USAGE = `usage:
41
- seq-studio auth pat create --name <n> [--scopes repo:read,repo:write] [-e env]
42
+ seq-studio auth pat create --name <n> -e <env> [--scopes repo:read,repo:write]
42
43
  [--expires 7d|30d|90d|1y|never] [--store-credentials]
43
- seq-studio auth pat list [-e env]
44
- seq-studio auth pat revoke <id> [-e env] [--yes]
44
+ seq-studio auth pat list -e <env>
45
+ seq-studio auth pat revoke <id> -e <env> [--yes]
45
46
 
46
47
  Issue a personal access token for git clone / git push against the platform
47
48
  git service.
48
49
 
49
- Authenticate with \`seq-studio login\`, then run \`auth pat create\`.
50
+ Authenticate with \`seq-studio login\`, then run \`auth pat create -e <env>\`.
50
51
  Alternatively, open Atlas → Settings → Tokens:
51
52
  https://<atlas-host>/settings/tokens
52
53
  Sign in, create a token (repo:read / repo:write), copy once, then:
@@ -55,7 +56,7 @@ export const AUTH_USAGE = `usage:
55
56
  On create the raw token is printed ONCE — store it; Atlas cannot re-show it.
56
57
  Git Basic auth: any username (e.g. git), PAT as the password.
57
58
 
58
- Flags: -e/--env <env> (see: seq-studio envs list)
59
+ Flags: -e/--env <env> (required; see: seq-studio envs list)
59
60
  `;
60
61
  async function authContext(args) {
61
62
  if (args.flags.env === true || args.flags.e === true) {
@@ -66,6 +67,10 @@ async function authContext(args) {
66
67
  }
67
68
  catch (err) {
68
69
  const message = err instanceof Error ? err.message : String(err);
70
+ // Missing `-e` is actionable on its own — don't bury it under login/PAT
71
+ // setup advice (that wrap is for real auth/token failures).
72
+ if (message === REQUIRE_EXPLICIT_ENV_MESSAGE)
73
+ throw err;
69
74
  // buildContext failed before we know the env URL; offer CLI login and
70
75
  // render the tokens-page URLs from the environments visible to this
71
76
  // identity (third-party / OpCo developers only see their own hosts).
@@ -119,7 +124,7 @@ export function resolveExpiresAt(choice) {
119
124
  export async function authPatCreateCommand(args) {
120
125
  const name = stringFlag(args.flags, 'name');
121
126
  if (!name) {
122
- console.error('usage: seq-studio auth pat create --name <n> [--scopes ...] [-e env]');
127
+ console.error('usage: seq-studio auth pat create --name <n> -e <env> [--scopes ...]');
123
128
  return 1;
124
129
  }
125
130
  if (name.length < 3) {
@@ -193,7 +198,7 @@ export async function authPatListCommand(args) {
193
198
  export async function authPatRevokeCommand(args) {
194
199
  const id = args.positional[0];
195
200
  if (!id) {
196
- console.error('usage: seq-studio auth pat revoke <id> [-e env] [--yes]');
201
+ console.error('usage: seq-studio auth pat revoke <id> -e <env> [--yes]');
197
202
  return 1;
198
203
  }
199
204
  const ctx = await authContext(args);