@sequenceholdings/artifact-studio 0.1.12 → 0.2.0

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 (43) hide show
  1. package/VERSION-PIN.md +54 -0
  2. package/dist/active-deploy-policy.d.ts +36 -0
  3. package/dist/active-deploy-policy.js +66 -0
  4. package/dist/api.js +59 -13
  5. package/dist/artifact-ignore.d.ts +16 -0
  6. package/dist/artifact-ignore.js +38 -0
  7. package/dist/atlas-ui-css-plugin.d.ts +15 -0
  8. package/dist/atlas-ui-css-plugin.js +69 -0
  9. package/dist/build-subprocess.d.ts +3 -1
  10. package/dist/build-subprocess.js +28 -6
  11. package/dist/build.d.ts +25 -0
  12. package/dist/build.js +97 -50
  13. package/dist/child-environment.d.ts +5 -0
  14. package/dist/child-environment.js +83 -0
  15. package/dist/cli.d.ts +40 -1
  16. package/dist/cli.js +173 -87
  17. package/dist/config.d.ts +0 -9
  18. package/dist/config.js +0 -18
  19. package/dist/deployment-validation.d.ts +9 -0
  20. package/dist/deployment-validation.js +70 -0
  21. package/dist/git-clone.js +16 -10
  22. package/dist/peer-versions.d.ts +19 -0
  23. package/dist/peer-versions.js +44 -0
  24. package/dist/prepare-build.d.ts +2 -0
  25. package/dist/prepare-build.js +27 -7
  26. package/dist/project.d.ts +3 -0
  27. package/dist/project.js +22 -6
  28. package/dist/runtime-chunks.d.ts +4 -0
  29. package/dist/runtime-chunks.js +15 -0
  30. package/dist/sdk.d.ts +10 -0
  31. package/dist/source-resolver.d.ts +8 -5
  32. package/dist/source-resolver.js +81 -32
  33. package/dist/templates/react-vite/AGENTS.md +90 -0
  34. package/dist/templates/react-vite/CLAUDE.md +4 -63
  35. package/dist/templates/react-vite/src/main.tsx +7 -1
  36. package/dist/templates/react-vite/src/styles.css +11 -0
  37. package/package.json +18 -5
  38. package/templates/react-vite/AGENTS.md +90 -0
  39. package/templates/react-vite/CLAUDE.md +4 -63
  40. package/templates/react-vite/src/main.tsx +7 -1
  41. package/templates/react-vite/src/styles.css +11 -0
  42. package/dist/auth.d.ts +0 -2
  43. package/dist/auth.js +0 -129
package/dist/cli.js CHANGED
@@ -3,21 +3,22 @@ import { existsSync } from 'node:fs';
3
3
  import { dirname, join, relative, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { getJson, getJsonOr404, postJson } from './api.js';
6
- import { ENV_URLS, readLocalConfig, resolveEnvironment, writeLocalConfig, writeTokenConfig, } from './config.js';
7
- import { buildArtifactStudioProject, writeBuildArtifact } from './build.js';
6
+ import { ENV_URLS, readLocalConfig, resolveEnvironment, writeLocalConfig, } from './config.js';
7
+ import { writeBuildArtifact } from './build.js';
8
8
  import { buildArtifactStudioProjectIsolated } from './build-subprocess.js';
9
+ import { resolveArtifactStudioPeerVersions, } from './peer-versions.js';
9
10
  import { prepareArtifactBuildRoot } from './prepare-build.js';
10
11
  import { parseArtifactStudioManifest } from './manifest.js';
11
12
  import { hasArtifactStudioManifest, readArtifactStudioSource } from './project.js';
12
13
  import { localGitMetadata, parseSourceSpec, resolveArtifactSource, } from './source-resolver.js';
13
- import { getAccessToken, loginWithPkce } from './auth.js';
14
14
  import { acquireDevLock } from './dev-lock.js';
15
15
  import { runWatchLoop } from './watch-loop.js';
16
16
  import { hashLinkedSources, parseLinks, rebuildLinkedDep, resolveLinkedDeps, syncLinkedDist, } from './dev-link.js';
17
17
  import { lintArtifactSandbox, reportSandboxWarnings } from './sandbox-lint.js';
18
+ import { assertActiveDeployProvenance } from './active-deploy-policy.js';
18
19
  import yaml from 'js-yaml';
19
20
  const COMMANDS = [
20
- 'init', 'login', 'logout', 'whoami', 'link', 'env', 'status', 'list', 'show',
21
+ 'init', 'whoami', 'link', 'env', 'status', 'list', 'show',
21
22
  'validate', 'build', 'plan', 'deploy', 'dev', 'promote', 'pull', 'rollback',
22
23
  ];
23
24
  const USAGE = `usage:
@@ -38,18 +39,29 @@ const USAGE = `usage:
38
39
  seq-studio artifact rollback <deployment-id> --env <env>
39
40
 
40
41
  Source: a local [dir] (default), a platform git-service repo (--repo <ns>/<name>),
41
- or any git URL (--git-url <url>). --ref picks a branch/tag/commit (default: the
42
+ or a public HTTPS git URL (--git-url <url>). --ref picks a branch/tag/commit (default: the
42
43
  repo's default branch). The target project comes from the source's
43
44
  artifact.bundle.yml project_id.
44
45
 
45
- --repo clones over smart-HTTP and REQUIRES a git PAT in ATLAS_GIT_PAT
46
- (repo:read scope)create one with \`seq-studio auth pat create --scopes
47
- repo:read\` or in Atlas → Settings → Tokens. It's the same token you clone the
48
- repo with. (--env + seq-studio login are still needed to resolve the repo and
49
- upload the deployment.)
46
+ Official (active) deploys require a clean \`main\` checkout with a resolved
47
+ git commitmerge first, then \`deploy --repo …\` (or a clean local \`main\`
48
+ checkout). Feature branches and dirty trees are preview-only
49
+ (\`artifact dev\` / preview channel).
50
+
51
+ Interactive --repo builds clone over smart-HTTP and require a repo:read git
52
+ PAT in ATLAS_GIT_PAT — create one with \`seq-studio auth pat create --scopes
53
+ repo:read\` or in Atlas → Settings → Tokens. Headless M2M builds use JSON
54
+ materialize and accept only platform-managed --repo sources. (--env +
55
+ seq-studio login are still needed to resolve the repo and upload the
56
+ deployment.)
50
57
 
51
58
  Authenticate with: seq-studio login`;
52
59
  export async function runCli(argv = process.argv.slice(2)) {
60
+ if (argv.some((arg) => arg === '--token' || arg.startsWith('--token='))) {
61
+ console.error('Artifact Studio does not accept --token bearer injection. ' +
62
+ 'Authenticate with seq-studio login.');
63
+ return 1;
64
+ }
53
65
  const parsed = parseArgs(argv);
54
66
  if ('error' in parsed) {
55
67
  console.error(parsed.error);
@@ -57,8 +69,6 @@ export async function runCli(argv = process.argv.slice(2)) {
57
69
  }
58
70
  switch (parsed.command) {
59
71
  case 'init': return initCommand(parsed);
60
- case 'login': return loginCommand(parsed);
61
- case 'logout': return logoutCommand();
62
72
  case 'whoami': return whoamiCommand(parsed);
63
73
  case 'link': return linkCommand(parsed);
64
74
  case 'env': return envCommand(parsed);
@@ -119,22 +129,6 @@ async function initCommand(args) {
119
129
  console.log('Next: seq-studio login && seq-studio artifact dev --env <env> (see: seq-studio envs list)');
120
130
  return 0;
121
131
  }
122
- async function loginCommand(args) {
123
- const token = typeof args.flags.token === 'string' ? args.flags.token : undefined;
124
- if (token) {
125
- await writeTokenConfig({ accessToken: token });
126
- console.log('[seq-studio] saved access token');
127
- return 0;
128
- }
129
- await loginWithPkce();
130
- console.log('[seq-studio] logged in');
131
- return 0;
132
- }
133
- async function logoutCommand() {
134
- await writeTokenConfig({});
135
- console.log('[seq-studio] logged out');
136
- return 0;
137
- }
138
132
  async function whoamiCommand(args) {
139
133
  const env = await envFromArgs(args);
140
134
  const token = await getRequiredToken(args);
@@ -315,25 +309,31 @@ async function showCommand(args) {
315
309
  const deployment = project.activeDeployment;
316
310
  if (deployment) {
317
311
  console.log(` active deployment:`);
318
- console.log(` version: ${deployment.version}`);
319
- console.log(` sourceHash: ${deployment.sourceHash}`);
320
- if (deployment.gitCommit) {
321
- const dirty = deployment.gitDirty ? ' (dirty)' : '';
322
- const branch = deployment.gitBranch ? ` on ${deployment.gitBranch}` : '';
323
- console.log(` git: ${deployment.gitCommit.slice(0, 12)}${branch}${dirty}`);
324
- }
325
- if (deployment.deployMessage)
326
- console.log(` message: ${deployment.deployMessage}`);
327
- if (deployment.createdAt)
328
- console.log(` deployedAt: ${deployment.createdAt}`);
329
- if (deployment.createdBy)
330
- console.log(` deployedBy: ${deployment.createdBy}`);
312
+ logActiveDeploymentDetails(deployment);
331
313
  }
332
314
  else {
333
315
  console.log(` active deployment: none`);
334
316
  }
335
317
  return 0;
336
318
  }
319
+ function logActiveDeploymentDetails(deployment) {
320
+ console.log(` version: ${deployment.version}`);
321
+ console.log(` sourceHash: ${deployment.sourceHash}`);
322
+ if (deployment.gitCommit) {
323
+ const dirty = deployment.gitDirty ? ' (dirty)' : '';
324
+ const branch = deployment.gitBranch ? ` on ${deployment.gitBranch}` : '';
325
+ console.log(` git: ${deployment.gitCommit.slice(0, 12)}${branch}${dirty}`);
326
+ }
327
+ if (deployment.cliVersion || deployment.atlasUiVersion) {
328
+ console.log(` peers: artifact-studio@${deployment.cliVersion ?? '?'} atlas-ui@${deployment.atlasUiVersion ?? '?'}`);
329
+ }
330
+ if (deployment.deployMessage)
331
+ console.log(` message: ${deployment.deployMessage}`);
332
+ if (deployment.createdAt)
333
+ console.log(` deployedAt: ${deployment.createdAt}`);
334
+ if (deployment.createdBy)
335
+ console.log(` deployedBy: ${deployment.createdBy}`);
336
+ }
337
337
  async function validateCommand(args) {
338
338
  const dir = resolve(args.positional[0] ?? '.');
339
339
  const source = await readArtifactStudioSource(dir);
@@ -345,15 +345,23 @@ async function validateCommand(args) {
345
345
  }
346
346
  async function buildPreparedArtifactSource(source) {
347
347
  await prepareArtifactBuildRoot(source.dir, { remote: source.remote });
348
- return buildArtifactStudioProject(source.dir);
348
+ return buildArtifactStudioProjectIsolated(source.dir);
349
349
  }
350
350
  async function buildCommand(args) {
351
351
  const spec = parseSourceSpec(args);
352
- // build is offline for a local/git-url source; only a git-service source
353
- // needs an env + token to fetch the tree.
354
- const remote = spec.kind === 'git-service'
355
- ? { baseUrl: (await envFromArgs(args)).url, token: await getRequiredToken(args) }
356
- : {};
352
+ // Local builds stay offline. Public git URLs need no bearer token, but an
353
+ // optional auth lookup still identifies a selected M2M principal so the
354
+ // source policy can reject arbitrary CI input before cloning.
355
+ const auth = spec.kind === 'git-service'
356
+ ? await getRequiredAuth(args)
357
+ : spec.kind === 'git-url'
358
+ ? await getOptionalAuth(args)
359
+ : null;
360
+ const remote = spec.kind === 'git-service' && auth
361
+ ? { authMode: auth.authMode, baseUrl: (await envFromArgs(args)).url, token: auth.token }
362
+ : auth
363
+ ? { authMode: auth.authMode }
364
+ : {};
357
365
  const source = await resolveArtifactSource(spec, remote);
358
366
  try {
359
367
  const result = await buildPreparedArtifactSource(source);
@@ -364,7 +372,8 @@ async function buildCommand(args) {
364
372
  await writeBuildArtifact(result, out);
365
373
  console.log(`[seq-studio] built ${result.manifest.bundle.name}`);
366
374
  console.log(`[seq-studio] source: ${result.sourceHash}`);
367
- console.log(`[seq-studio] bundle: ${result.bundleHash}`);
375
+ console.log(`[seq-studio] bundle: ${result.bundleHash} (${result.bundleFormat}, ${result.chunks.length} chunks)`);
376
+ console.log(`[seq-studio] peers: artifact-studio@${result.peerVersions.cliVersion} atlas-ui@${result.peerVersions.atlasUiVersion}`);
368
377
  console.log(`[seq-studio] wrote ${out}`);
369
378
  reportSandboxWarnings(lintArtifactSandbox(result.files));
370
379
  return 0;
@@ -375,9 +384,14 @@ async function buildCommand(args) {
375
384
  }
376
385
  async function planCommand(args) {
377
386
  const env = await envFromArgs(args);
378
- const token = await getOptionalToken(args);
387
+ const auth = await getOptionalAuth(args);
388
+ const token = auth?.token ?? null;
379
389
  const spec = parseSourceSpec(args);
380
- const source = await resolveArtifactSource(spec, { baseUrl: env.url, token });
390
+ const source = await resolveArtifactSource(spec, {
391
+ ...(auth ? { authMode: auth.authMode } : {}),
392
+ baseUrl: env.url,
393
+ token,
394
+ });
381
395
  try {
382
396
  const result = await buildPreparedArtifactSource(source);
383
397
  console.log(`[seq-studio] plan ${result.manifest.artifact.project_id} -> ${env.name}`);
@@ -388,12 +402,29 @@ async function planCommand(args) {
388
402
  return 0;
389
403
  }
390
404
  const remote = await fetchRemoteProject({ env: env.url, token, slug: result.manifest.artifact.project_id });
391
- if (!remote)
405
+ if (!remote) {
392
406
  console.log(' action: create project and deploy');
393
- else if (remote.activeDeployment?.sourceHash === result.sourceHash)
407
+ return 0;
408
+ }
409
+ // Mirror the deploy --skip-unchanged decision exactly: identical source
410
+ // with a different (or unrecorded) CLI/atlas-ui pin still redeploys, so
411
+ // plan must not report no-op there.
412
+ const active = remote.activeDeployment;
413
+ if (activeDeploymentMatchesSkipPin({
414
+ active,
415
+ sourceHash: result.sourceHash,
416
+ peerVersions: result.peerVersions,
417
+ })) {
394
418
  console.log(' action: no-op');
395
- else
396
- console.log(` action: deploy over ${remote.activeDeployment?.version ?? 'no active deployment'}`);
419
+ }
420
+ else if (active && active.sourceHash === result.sourceHash) {
421
+ console.log(` action: deploy over ${active.version} ` +
422
+ `(source unchanged; pin moves ${active.cliVersion ?? 'unrecorded'}/${active.atlasUiVersion ?? 'unrecorded'} ` +
423
+ `→ ${result.peerVersions.cliVersion}/${result.peerVersions.atlasUiVersion})`);
424
+ }
425
+ else {
426
+ console.log(` action: deploy over ${active?.version ?? 'no active deployment'}`);
427
+ }
397
428
  return 0;
398
429
  }
399
430
  finally {
@@ -402,25 +433,41 @@ async function planCommand(args) {
402
433
  }
403
434
  async function deployCommand(args) {
404
435
  const env = await envFromArgs(args);
405
- const token = await getRequiredToken(args);
436
+ const auth = await getRequiredAuth(args);
437
+ const { token } = auth;
406
438
  const spec = parseSourceSpec(args);
407
439
  const explicitProjectId = typeof args.flags.project === 'string' ? args.flags.project : null;
408
- const source = await resolveArtifactSource(spec, { baseUrl: env.url, token });
440
+ const source = await resolveArtifactSource(spec, {
441
+ authMode: auth.authMode,
442
+ baseUrl: env.url,
443
+ token,
444
+ });
409
445
  try {
446
+ // Fail closed before build / project create — active deploys must already
447
+ // look like a clean `main` checkout. Preview (`dev`) never hits this path.
448
+ assertActiveDeployProvenance(source.provenance);
410
449
  // --skip-unchanged: hash the raw source (no build needed) and no-op when
411
- // the remote active deployment already matches. Lets CI redeploy every
412
- // artifact on every release without paying a vite build + upload for the
413
- // unchanged ones. sourceHash covers manifest + all source files, so any
414
- // real change falls through to a full deploy.
450
+ // the remote active deployment already matches *and* was built with the
451
+ // same CLI/atlas-ui pin. Lets CI redeploy every artifact on every release
452
+ // without paying a vite build + upload for the unchanged ones. sourceHash
453
+ // covers manifest + all source files; peer versions cover the force-alias
454
+ // inputs (DES-254) so a newer CLI still rebuilds identical source.
415
455
  if (args.flags['skip-unchanged'] === true) {
416
456
  const unbuilt = await readArtifactStudioSource(source.dir);
417
457
  const slug = unbuilt.manifest.artifact.project_id;
418
458
  const remote = explicitProjectId
419
459
  ? (await getJsonOr404({ baseUrl: env.url, token, path: `/api/artifact-studio/projects/${explicitProjectId}` }))?.project ?? null
420
460
  : await fetchRemoteProject({ env: env.url, token, slug });
421
- if (remote?.activeDeployment?.sourceHash === unbuilt.sourceHash) {
461
+ const peerVersions = resolveArtifactStudioPeerVersions();
462
+ const active = remote?.activeDeployment;
463
+ if (activeDeploymentMatchesSkipPin({
464
+ active,
465
+ sourceHash: unbuilt.sourceHash,
466
+ peerVersions,
467
+ }) && active) {
422
468
  console.log(`[seq-studio] ${slug} unchanged on ${env.name} ` +
423
- `(active ${remote.activeDeployment.version} matches source ${unbuilt.sourceHash.slice(0, 12)}) skipping deploy`);
469
+ `(active ${active.version} matches source ${unbuilt.sourceHash.slice(0, 12)} ` +
470
+ `+ peers artifact-studio@${peerVersions.cliVersion} atlas-ui@${peerVersions.atlasUiVersion}) — skipping deploy`);
424
471
  return 0;
425
472
  }
426
473
  }
@@ -436,6 +483,7 @@ async function deployCommand(args) {
436
483
  linkConfigDir: spec.kind === 'local' ? source.dir : null,
437
484
  allowCreate: args.flags['no-create'] !== true,
438
485
  explicitProjectId,
486
+ envName: env.name,
439
487
  });
440
488
  const deployment = await uploadDeployment({ env: env.url, token, projectId: project.id, result, channel: 'active', provenance: source.provenance });
441
489
  console.log(`[seq-studio] deployed ${project.title} ${deployment.version}`);
@@ -520,7 +568,9 @@ async function devCommand(args) {
520
568
  // Resolve per push so the token refreshes across a long watch session.
521
569
  const token = await getRequiredToken(args);
522
570
  if (!projectId) {
523
- const project = await ensureRemoteProject({ env: env.url, token, result, linkConfigDir: dir });
571
+ // envName so a first-time `artifact dev` creation honors the manifest
572
+ // visibility too (and notes drift, once, when the project pre-exists).
573
+ const project = await ensureRemoteProject({ env: env.url, token, result, linkConfigDir: dir, envName: env.name });
524
574
  projectId = project.id;
525
575
  }
526
576
  const deployment = await uploadDeployment({
@@ -684,7 +734,36 @@ async function rollbackCommand(args) {
684
734
  console.log(`[seq-studio] rolled back to ${deploymentId}`);
685
735
  return 0;
686
736
  }
687
- async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCreate = true, explicitProjectId = null }) {
737
+ /**
738
+ * Whether `--skip-unchanged` may no-op: same source *and* same CLI/atlas-ui
739
+ * pin. Missing peer fields on older rows force a rebuild so the pin becomes
740
+ * observable (DES-254).
741
+ */
742
+ export function activeDeploymentMatchesSkipPin({ active, sourceHash, peerVersions, }) {
743
+ if (!active)
744
+ return false;
745
+ if (active.sourceHash !== sourceHash)
746
+ return false;
747
+ if (!active.cliVersion || !active.atlasUiVersion)
748
+ return false;
749
+ return (active.cliVersion === peerVersions.cliVersion &&
750
+ active.atlasUiVersion === peerVersions.atlasUiVersion);
751
+ }
752
+ /**
753
+ * Deploys never change a live project's visibility — the Studio sharing UI /
754
+ * PATCH own that. The manifest's targets.<env>.visibility is honored at
755
+ * project CREATION only; on drift, say so instead of silently ignoring the
756
+ * field (which is how it read as decorative for a year).
757
+ */
758
+ function warnOnVisibilityDrift({ project, declared, envName }) {
759
+ if (!declared || !envName || !project.visibility || declared === project.visibility)
760
+ return;
761
+ console.warn(`[seq-studio] note: artifact.bundle.yml targets.${envName}.visibility is "${declared}" but the project is ` +
762
+ `"${project.visibility}" on this environment — deploys never change visibility. Update it in Studio sharing ` +
763
+ `(or PATCH /api/artifact-studio/projects/${project.id}) if the manifest is what you intend.`);
764
+ }
765
+ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCreate = true, explicitProjectId = null, envName = null }) {
766
+ const declaredVisibility = envName ? result.manifest.targets?.[envName]?.visibility : undefined;
688
767
  if (explicitProjectId) {
689
768
  const data = await getJsonOr404({ baseUrl: env, token, path: `/api/artifact-studio/projects/${explicitProjectId}` });
690
769
  if (!data)
@@ -693,19 +772,23 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
693
772
  throw new Error(`--project ${explicitProjectId} resolves to slug "${data.project.slug}" but this source's ` +
694
773
  `artifact.bundle.yml declares project_id "${result.manifest.artifact.project_id}" — refusing to deploy over a different project`);
695
774
  }
775
+ warnOnVisibilityDrift({ project: data.project, declared: declaredVisibility, envName });
696
776
  return data.project;
697
777
  }
698
778
  const config = linkConfigDir ? await readLocalConfig(linkConfigDir) : {};
699
779
  if (config.projectId) {
700
780
  const data = await getJsonOr404({ baseUrl: env, token, path: `/api/artifact-studio/projects/${config.projectId}` });
701
- if (data)
781
+ if (data) {
782
+ warnOnVisibilityDrift({ project: data.project, declared: declaredVisibility, envName });
702
783
  return data.project;
784
+ }
703
785
  console.warn(`[seq-studio] cached projectId ${config.projectId} not found on ${env} — relinking by slug`);
704
786
  }
705
787
  const existing = await fetchRemoteProject({ env, token, slug: result.manifest.artifact.project_id });
706
788
  if (existing) {
707
789
  if (linkConfigDir)
708
790
  await writeLocalConfig({ ...config, projectId: existing.id, projectSlug: existing.slug }, linkConfigDir);
791
+ warnOnVisibilityDrift({ project: existing, declared: declaredVisibility, envName });
709
792
  return existing;
710
793
  }
711
794
  if (!allowCreate) {
@@ -721,6 +804,7 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
721
804
  title: result.manifest.artifact.title,
722
805
  slug: result.manifest.artifact.project_id,
723
806
  description: result.manifest.artifact.description ?? null,
807
+ ...(declaredVisibility ? { visibility: declaredVisibility } : {}),
724
808
  },
725
809
  });
726
810
  if (linkConfigDir)
@@ -746,11 +830,15 @@ async function uploadDeployment({ env, token, projectId, result, channel, previe
746
830
  manifest: result.manifest,
747
831
  files: result.files,
748
832
  bundle: result.bundle,
833
+ chunks: result.chunks.map((chunk) => ({ fileName: chunk.fileName, content: chunk.code })),
834
+ bundleFormat: result.bundleFormat,
749
835
  sourceMap: result.sourceMap,
750
836
  ...(version ? { version } : {}),
751
837
  channel,
752
838
  previewKey,
753
839
  deployMessage: message ?? null,
840
+ cliVersion: result.peerVersions.cliVersion,
841
+ atlasUiVersion: result.peerVersions.atlasUiVersion,
754
842
  ...provenance,
755
843
  },
756
844
  });
@@ -771,38 +859,36 @@ async function fetchRemoteProject({ env, token, slug, disambiguator }) {
771
859
  }
772
860
  return matches[0] ?? null;
773
861
  }
774
- async function envFromArgs(args) {
862
+ export async function getConfiguredDefaultEnv() {
775
863
  const config = await readLocalConfig().catch(() => ({ defaultEnv: undefined }));
776
- return resolveEnvironment(typeof args.flags.env === 'string' ? args.flags.env : undefined, config.defaultEnv);
864
+ return config.defaultEnv;
865
+ }
866
+ async function envFromArgs(args) {
867
+ return resolveEnvironment(typeof args.flags.env === 'string' ? args.flags.env : undefined, await getConfiguredDefaultEnv());
777
868
  }
778
- /**
779
- * Optional refreshing token provider, injected by an embedder (e.g. seq-studio)
780
- * that owns the shared seqapi token file and can mint a fresh access token on
781
- * demand. Consulted before the static ARTIFACT_STUDIO_TOKEN env var so a
782
- * long-running `dev` watcher keeps deploying past the initial token's TTL —
783
- * the env var is captured once at process start and never refreshes.
784
- */
785
869
  let tokenProvider = null;
786
870
  export function setTokenProvider(provider) {
787
871
  tokenProvider = provider;
788
872
  }
789
- async function getRequiredToken(args) {
790
- const token = await getOptionalToken(args);
791
- if (!token)
792
- throw new Error('Missing token. Run seq-studio login or pass --token.');
793
- return token;
873
+ async function getRequiredToken(_args) {
874
+ return (await getRequiredAuth(_args)).token;
875
+ }
876
+ async function getRequiredAuth(_args) {
877
+ const auth = await resolveAuth({ allowInteractiveLogin: true });
878
+ if (!auth)
879
+ throw new Error('Missing token. Authenticate with seq-studio login.');
880
+ return auth;
881
+ }
882
+ async function getOptionalAuth(_args) {
883
+ return resolveAuth({ allowInteractiveLogin: false });
794
884
  }
795
- async function getOptionalToken(args) {
796
- if (typeof args.flags.token === 'string')
797
- return args.flags.token;
885
+ async function resolveAuth(options) {
798
886
  if (tokenProvider) {
799
- const provided = await tokenProvider();
887
+ const provided = await tokenProvider(options);
800
888
  if (provided)
801
889
  return provided;
802
890
  }
803
- if (process.env.ARTIFACT_STUDIO_TOKEN)
804
- return process.env.ARTIFACT_STUDIO_TOKEN;
805
- return getAccessToken();
891
+ return null;
806
892
  }
807
893
  async function replaceInFile(path, pattern, value) {
808
894
  const content = await import('node:fs/promises').then((fs) => fs.readFile(path, 'utf8'));
@@ -831,4 +917,4 @@ function warnIfInlinedManifestDriftsFromBundle(result) {
831
917
  // This module is a pure library export. `seq-studio artifact` imports
832
918
  // `runCli` from `@sequenceholdings/artifact-studio/cli` (see
833
919
  // shared/services/studio-cli/README.md) and invokes it after injecting a
834
- // shared base URL + Bearer token.
920
+ // shared base URL + internal token provider.
package/dist/config.d.ts CHANGED
@@ -11,19 +11,10 @@ export interface LocalProjectConfig {
11
11
  defaultEnv?: string;
12
12
  previewKey?: string;
13
13
  }
14
- export interface TokenConfig {
15
- accessToken?: string;
16
- refreshToken?: string;
17
- expiresAt?: number;
18
- }
19
- export declare function globalConfigDir(): string;
20
- export declare function tokenConfigPath(): string;
21
14
  export declare function localConfigPath(cwd?: string): string;
22
15
  export declare function devLockPath(cwd?: string): string;
23
16
  export declare function readLocalConfig(cwd?: string): Promise<LocalProjectConfig>;
24
17
  export declare function writeLocalConfig(config: LocalProjectConfig, cwd?: string): Promise<void>;
25
- export declare function readTokenConfig(): Promise<TokenConfig>;
26
- export declare function writeTokenConfig(config: TokenConfig): Promise<void>;
27
18
  export declare function knownEnvUrls(): Record<string, string>;
28
19
  export declare function resolveEnvironment(name: string | undefined, fallback?: string): {
29
20
  name: string;
package/dist/config.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { dirname, join, resolve } from 'node:path';
4
- import { homedir } from 'node:os';
5
4
  /**
6
5
  * Only `local` ships in the published package. Deployed-environment URLs
7
6
  * are resolved by the embedding CLI (seq-studio), which discovers the
@@ -11,12 +10,6 @@ import { homedir } from 'node:os';
11
10
  export const ENV_URLS = {
12
11
  local: 'http://localhost:5001',
13
12
  };
14
- export function globalConfigDir() {
15
- return join(homedir(), '.config', 'sequence-artifact-studio');
16
- }
17
- export function tokenConfigPath() {
18
- return join(globalConfigDir(), 'tokens.json');
19
- }
20
13
  export function localConfigPath(cwd = process.cwd()) {
21
14
  return join(resolve(cwd), '.artifact-studio', 'config.json');
22
15
  }
@@ -34,17 +27,6 @@ export async function writeLocalConfig(config, cwd = process.cwd()) {
34
27
  await mkdir(dirname(path), { recursive: true });
35
28
  await writeFile(path, JSON.stringify(config, null, 2) + '\n', 'utf8');
36
29
  }
37
- export async function readTokenConfig() {
38
- const path = tokenConfigPath();
39
- if (!existsSync(path))
40
- return {};
41
- return JSON.parse(await readFile(path, 'utf8'));
42
- }
43
- export async function writeTokenConfig(config) {
44
- const path = tokenConfigPath();
45
- await mkdir(dirname(path), { recursive: true });
46
- await writeFile(path, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
47
- }
48
30
  /**
49
31
  * Environment map injected by the embedding CLI (seq-studio sets
50
32
  * ARTIFACT_STUDIO_ENV_URLS to the JSON name→url map of environments visible
@@ -0,0 +1,9 @@
1
+ export declare function validateDeploymentBaseUrl(input: string): string;
2
+ export declare function validateDeploymentAudience({ audience, baseUrl, }: {
3
+ audience: string;
4
+ baseUrl: string;
5
+ }): string;
6
+ export declare function authenticatedRequestUrl({ baseUrl, path, }: {
7
+ baseUrl: string;
8
+ path: string;
9
+ }): string;
@@ -0,0 +1,70 @@
1
+ const TRUSTED_DEPLOYMENT_DOMAIN = 'seqholdings.com';
2
+ export function validateDeploymentBaseUrl(input) {
3
+ if (input !== input.trim()) {
4
+ throw new Error(`Invalid deployment URL '${input}'.`);
5
+ }
6
+ let parsed;
7
+ try {
8
+ parsed = new URL(input);
9
+ }
10
+ catch {
11
+ throw new Error(`Invalid deployment URL '${input}'.`);
12
+ }
13
+ const hostname = parsed.hostname.toLowerCase();
14
+ const originOnly = !parsed.username &&
15
+ !parsed.password &&
16
+ (parsed.pathname === '' || parsed.pathname === '/') &&
17
+ !parsed.search &&
18
+ !parsed.hash;
19
+ const trustedHttps = parsed.protocol === 'https:' &&
20
+ (hostname === TRUSTED_DEPLOYMENT_DOMAIN ||
21
+ hostname.endsWith(`.${TRUSTED_DEPLOYMENT_DOMAIN}`)) &&
22
+ parsed.port === '';
23
+ const localHttp = parsed.protocol === 'http:' &&
24
+ ['localhost', '127.0.0.1', '[::1]'].includes(hostname);
25
+ if (!originOnly || (!trustedHttps && !localHttp)) {
26
+ throw new Error(`Untrusted deployment URL '${input}': expected an HTTPS origin under ` +
27
+ `*.${TRUSTED_DEPLOYMENT_DOMAIN}, or an HTTP localhost/loopback origin.`);
28
+ }
29
+ return trustedHttps ? `https://${hostname}` : parsed.origin.toLowerCase();
30
+ }
31
+ export function validateDeploymentAudience({ audience, baseUrl, }) {
32
+ const expected = `${baseUrl}/api`;
33
+ if (audience !== expected) {
34
+ throw new Error(`Untrusted Auth0 audience '${audience}': deployment ${baseUrl} must use '${expected}'.`);
35
+ }
36
+ return audience;
37
+ }
38
+ export function authenticatedRequestUrl({ baseUrl, path, }) {
39
+ const trustedOrigin = validateDeploymentBaseUrl(baseUrl);
40
+ if (!path.startsWith('/')) {
41
+ throw new Error(`Invalid authenticated request path '${path}': expected an absolute path.`);
42
+ }
43
+ if (path.startsWith('//')) {
44
+ throw new Error(`Invalid authenticated request path '${path}': scheme-relative paths are not allowed.`);
45
+ }
46
+ if ([...path].some((character) => character.charCodeAt(0) < 0x20)) {
47
+ throw new Error(`Invalid authenticated request path '${path}': control character.`);
48
+ }
49
+ const separatorIndex = path.search(/[?#]/);
50
+ const rawPathname = separatorIndex === -1 ? path : path.slice(0, separatorIndex);
51
+ const traversalCandidate = rawPathname
52
+ .replace(/%2e/gi, '.')
53
+ .replace(/%2f/gi, '/')
54
+ .replace(/%5c/gi, '\\');
55
+ if (traversalCandidate.includes('\\') ||
56
+ traversalCandidate.split('/').some((segment) => segment === '.' || segment === '..')) {
57
+ throw new Error(`Invalid authenticated request path '${path}': traversal is not allowed.`);
58
+ }
59
+ let request;
60
+ try {
61
+ request = new URL(path, `${trustedOrigin}/`);
62
+ }
63
+ catch {
64
+ throw new Error(`Invalid authenticated request path '${path}'.`);
65
+ }
66
+ if (request.origin.toLowerCase() !== trustedOrigin) {
67
+ throw new Error(`Untrusted authenticated request origin '${request.origin}': expected '${trustedOrigin}'.`);
68
+ }
69
+ return request.href;
70
+ }
package/dist/git-clone.js CHANGED
@@ -10,9 +10,10 @@
10
10
  * HTTPS clone URL plus the PAT separately.
11
11
  */
12
12
  import { spawn } from 'node:child_process';
13
- import { mkdtemp, rm, writeFile } from 'node:fs/promises';
13
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
14
14
  import { tmpdir } from 'node:os';
15
15
  import { join } from 'node:path';
16
+ import { createScrubbedChildEnv } from './child-environment.js';
16
17
  /** Read the git PAT from the environment (`ATLAS_GIT_PAT`), trimmed. */
17
18
  export function resolveGitPatFromEnv() {
18
19
  const value = process.env.ATLAS_GIT_PAT?.trim();
@@ -108,6 +109,7 @@ async function spawnGit({ args, env, cwd, timeoutMs = GIT_CLONE_TIMEOUT_MS, }) {
108
109
  */
109
110
  export async function withGitAskpass({ pat, fn, }) {
110
111
  const dir = await mkdtemp(join(tmpdir(), 'artifact-askpass-'));
112
+ const homeDir = join(dir, 'home');
111
113
  const scriptPath = join(dir, 'askpass.sh');
112
114
  const script = [
113
115
  '#!/bin/sh',
@@ -117,17 +119,21 @@ export async function withGitAskpass({ pat, fn, }) {
117
119
  'esac',
118
120
  '',
119
121
  ].join('\n');
122
+ await mkdir(homeDir, { recursive: true });
120
123
  await writeFile(scriptPath, script, { mode: 0o700 });
121
124
  try {
122
- return await fn({
123
- ...process.env,
124
- GIT_ASKPASS: scriptPath,
125
- GIT_TERMINAL_PROMPT: '0',
126
- SEQ_STUDIO_GIT_ASKPASS_PASSWORD: pat,
127
- GIT_CONFIG_COUNT: '1',
128
- GIT_CONFIG_KEY_0: 'credential.helper',
129
- GIT_CONFIG_VALUE_0: '',
130
- });
125
+ return await fn(createScrubbedChildEnv({
126
+ homeDir,
127
+ additionalEnv: {
128
+ GIT_ASKPASS: scriptPath,
129
+ GIT_TERMINAL_PROMPT: '0',
130
+ GIT_CONFIG_NOSYSTEM: '1',
131
+ SEQ_STUDIO_GIT_ASKPASS_PASSWORD: pat,
132
+ GIT_CONFIG_COUNT: '1',
133
+ GIT_CONFIG_KEY_0: 'credential.helper',
134
+ GIT_CONFIG_VALUE_0: '',
135
+ },
136
+ }));
131
137
  }
132
138
  finally {
133
139
  await rm(dir, { recursive: true, force: true });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Versions of the packages that actually land in an artifact bundle under the
3
+ * CLI atlas-ui stamp (DES-254). See `VERSION-PIN.md`.
4
+ *
5
+ * - `cliVersion` — `@sequenceholdings/artifact-studio` (the build package that
6
+ * owns `PEER_RESOLVE_ALIAS`).
7
+ * - `atlasUiVersion` — the `@sequenceholdings/atlas-ui` copy resolved from
8
+ * *that* package's dependency tree (not the artifact's package.json).
9
+ */
10
+ export interface ArtifactStudioPeerVersions {
11
+ cliVersion: string;
12
+ atlasUiVersion: string;
13
+ }
14
+ /**
15
+ * Resolve the building CLI + force-aliased atlas-ui versions from disk.
16
+ * Throws if either package.json is unreadable — a silent null here would
17
+ * undermine the observability DES-254 adds for the blast-radius scanner.
18
+ */
19
+ export declare function resolveArtifactStudioPeerVersions(): ArtifactStudioPeerVersions;