@sequenceholdings/artifact-studio 0.9.1 → 0.10.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 (54) hide show
  1. package/README.md +219 -0
  2. package/VERSION-PIN.md +15 -0
  3. package/dist/api.d.ts +5 -0
  4. package/dist/api.js +7 -0
  5. package/dist/artifact-env-bindings.d.ts +1 -1
  6. package/dist/artifact-env-bindings.js +5 -4
  7. package/dist/build-input-guards.d.ts +8 -0
  8. package/dist/build-input-guards.js +178 -0
  9. package/dist/build.js +14 -5
  10. package/dist/cli-content-readers.d.ts +8 -0
  11. package/dist/cli-content-readers.js +28 -0
  12. package/dist/cli.d.ts +1 -0
  13. package/dist/cli.js +38 -6
  14. package/dist/{env-binding-scan.d.ts → env-binding-manifest.d.ts} +1 -4
  15. package/dist/env-binding-manifest.js +56 -0
  16. package/dist/env-bindings.js +2 -34
  17. package/dist/git-service-client.d.ts +11 -0
  18. package/dist/git-service-client.js +10 -0
  19. package/dist/manifest-next.d.ts +6 -0
  20. package/dist/manifest-next.js +3 -0
  21. package/dist/manifest.d.ts +4 -0
  22. package/dist/manifest.js +1 -0
  23. package/dist/next/bridge.d.ts +15 -1
  24. package/dist/next/bridge.js +18 -1
  25. package/dist/next-source-includes.d.ts +50 -0
  26. package/dist/next-source-includes.js +68 -0
  27. package/dist/prepare-build.d.ts +10 -2
  28. package/dist/prepare-build.js +33 -3
  29. package/dist/repository-identity.d.ts +31 -0
  30. package/dist/repository-identity.js +41 -0
  31. package/dist/runtime-contracts.d.ts +1 -4
  32. package/dist/runtime-contracts.js +2 -15
  33. package/dist/server/api-client.d.ts +1 -0
  34. package/dist/server/api-client.js +13 -7
  35. package/dist/server/group-directory.d.ts +90 -0
  36. package/dist/server/group-directory.js +17 -0
  37. package/dist/server/groups.d.ts +2 -1
  38. package/dist/server/groups.js +2 -0
  39. package/dist/server/human-completion.d.ts +44 -0
  40. package/dist/server/human-completion.js +38 -0
  41. package/dist/server/processes.d.ts +19 -0
  42. package/dist/server/processes.js +11 -0
  43. package/dist/server/task-assignments.d.ts +81 -0
  44. package/dist/server/task-assignments.js +26 -0
  45. package/dist/server.d.ts +4 -0
  46. package/dist/source-boundary.d.ts +27 -6
  47. package/dist/source-boundary.js +44 -30
  48. package/dist/source-exclusions.d.ts +4 -0
  49. package/dist/source-exclusions.js +16 -0
  50. package/dist/source-resolver.js +2 -1
  51. package/dist/templates/react-vite/package.json +1 -1
  52. package/package.json +6 -2
  53. package/templates/react-vite/package.json +1 -1
  54. package/dist/env-binding-scan.js +0 -179
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { runContentReadersCommand } from './cli-content-readers.js';
1
2
  import { copyFile, cp, mkdir, writeFile } from 'node:fs/promises';
2
3
  import { constants, existsSync } from 'node:fs';
3
4
  import { dirname, join, relative, resolve } from 'node:path';
@@ -22,7 +23,7 @@ import yaml from 'js-yaml';
22
23
  import { warnOnRoleDefinitions } from './role-definitions-notice.js';
23
24
  const COMMANDS = [
24
25
  'init', 'whoami', 'link', 'env', 'status', 'list', 'show',
25
- 'validate', 'build', 'plan', 'deploy', 'dev', 'promote', 'pull', 'rollback',
26
+ 'content-readers', 'validate', 'build', 'plan', 'deploy', 'dev', 'promote', 'pull', 'rollback',
26
27
  ];
27
28
  const USAGE = `usage:
28
29
  seq-studio artifact init <dir> [--template react-vite|nextjs]
@@ -40,6 +41,7 @@ const USAGE = `usage:
40
41
  seq-studio artifact promote <deployment-id> --env <env>
41
42
  seq-studio artifact pull <project-id> --env <env> --out <dir>
42
43
  seq-studio artifact rollback <deployment-id> --env <env>
44
+ seq-studio artifact content-readers show|apply --project <id> --env <env> [--file bindings.json]
43
45
 
44
46
  Add --help or -h to any artifact command to print this usage without running it.
45
47
  init includes AGENTS.md pointing to your environment's live docs, with a
@@ -64,6 +66,7 @@ const USAGE = `usage:
64
66
  select the target environment. Next.js build/plan/deploy accept --source-env
65
67
  to read a Git Service repository from a separately authenticated environment.
66
68
  Log in to both environments; source read and target deploy permissions apply.
69
+ Source identity must be adopted and belong to the destination tenant.
67
70
  The target must support verified cross-environment builds. Source credentials
68
71
  are used only to fetch source, never passed to the build or stored with it.
69
72
 
@@ -110,6 +113,10 @@ export async function runCli(argv = process.argv.slice(2)) {
110
113
  case 'promote': return promoteCommand(parsed);
111
114
  case 'pull': return pullCommand(parsed);
112
115
  case 'rollback': return rollbackCommand(parsed);
116
+ case 'content-readers': {
117
+ const env = await envFromArgs(parsed);
118
+ return runContentReadersCommand({ action: parsed.positional.join(' '), projectId: parsed.flags.project, file: parsed.flags.file, baseUrl: env.url, token: await getRequiredToken(parsed) });
119
+ }
113
120
  }
114
121
  }
115
122
  function validateKnownOptions(args) {
@@ -124,6 +131,7 @@ function validateKnownOptions(args) {
124
131
  dev: { values: ['env', 'key', 'link', 'port'], booleans: ['once'] },
125
132
  promote: { values: ['env'], booleans: [] }, pull: { values: ['env', 'out'], booleans: [] },
126
133
  rollback: { values: ['env'], booleans: [] },
134
+ 'content-readers': { values: ['env', 'project', 'file'], booleans: [] },
127
135
  };
128
136
  const { values, booleans } = options[args.command];
129
137
  const allowed = new Set([...values, ...booleans]);
@@ -397,6 +405,19 @@ async function showCommand(args) {
397
405
  }
398
406
  return 0;
399
407
  }
408
+ function observedEvidenceLine(evidence) {
409
+ if ('atlasUi' in evidence) {
410
+ return `${evidence.channel} / ${evidence.integrityReceipt?.status ?? 'no receipt'} / ${evidence.toolchain.selection} toolchain ` +
411
+ `(atlas-ui ${evidence.atlasUi.contentHash.slice(0, 12)})`;
412
+ }
413
+ const toolchain = evidence.toolchain;
414
+ const parts = [
415
+ `Next.js ${toolchain?.next ?? '?'} on Node ${toolchain?.node ?? '?'}`,
416
+ evidence.builderImage ? `builder ${evidence.builderImage.replace(/^sha256:/, '').slice(0, 12)}` : undefined,
417
+ evidence.lockfileSha256 ? `lockfile ${evidence.lockfileSha256.slice(0, 12)}` : undefined,
418
+ ];
419
+ return parts.filter(Boolean).join(' / ');
420
+ }
400
421
  function logActiveDeploymentDetails(deployment) {
401
422
  console.log(` version: ${deployment.version}`);
402
423
  console.log(` sourceHash: ${deployment.sourceHash}`);
@@ -408,10 +429,8 @@ function logActiveDeploymentDetails(deployment) {
408
429
  if (deployment.cliVersion || deployment.atlasUiVersion) {
409
430
  console.log(` peers: artifact-studio@${deployment.cliVersion ?? '?'} atlas-ui@${deployment.atlasUiVersion ?? '?'}`);
410
431
  }
411
- if (deployment.observedBuildEvidence) {
412
- console.log(` observed: ${deployment.observedBuildEvidence.channel} / ${deployment.observedBuildEvidence.integrityReceipt?.status ?? 'no receipt'} / ${deployment.observedBuildEvidence.toolchain.selection} toolchain ` +
413
- `(atlas-ui ${deployment.observedBuildEvidence.atlasUi.contentHash.slice(0, 12)})`);
414
- }
432
+ if (deployment.observedBuildEvidence)
433
+ console.log(` observed: ${observedEvidenceLine(deployment.observedBuildEvidence)}`);
415
434
  if (deployment.deployMessage)
416
435
  console.log(` message: ${deployment.deployMessage}`);
417
436
  if (deployment.createdAt)
@@ -435,7 +454,10 @@ async function validateCommand(args) {
435
454
  return 0;
436
455
  }
437
456
  async function buildPreparedArtifactSource(source) {
438
- await prepareArtifactBuildRoot(source.dir, { remote: source.remote });
457
+ await prepareArtifactBuildRoot(source.dir, {
458
+ remote: source.remote,
459
+ ...(packageInstallUnavailable ? { installUnavailable: packageInstallUnavailable } : {}),
460
+ });
439
461
  return buildArtifactStudioProjectIsolated(source.dir);
440
462
  }
441
463
  function reportBuildDiagnostics(result) {
@@ -1066,6 +1088,16 @@ function assertSourceEnvironmentSupportsManifest(args) {
1066
1088
  throw new Error('--source-env is supported for Next.js repository builds only.');
1067
1089
  }
1068
1090
  let tokenProvider = null;
1091
+ /**
1092
+ * Embedder guidance for a host that cannot reach a package registry, such as an
1093
+ * agent sandbox. When set, a remote build whose package.json declares
1094
+ * production dependencies fails with it instead of attempting an install.
1095
+ * Next.js applications build in Atlas and are unaffected.
1096
+ */
1097
+ let packageInstallUnavailable = null;
1098
+ export function setPackageInstallUnavailable(guidance) {
1099
+ packageInstallUnavailable = guidance;
1100
+ }
1069
1101
  export function setTokenProvider(provider) {
1070
1102
  tokenProvider = provider;
1071
1103
  }
@@ -1,10 +1,7 @@
1
1
  import type { BindingSourceFile, NamedOrigin } from './env-bindings.js';
2
2
  export declare function originsDeclaredInFiles(files: readonly BindingSourceFile[]): NamedOrigin[];
3
3
  export declare function declaredTargets(content: string): Record<string, string>;
4
- export declare function contentContainsOrigin(content: string, origin: string, skippedLines: ReadonlySet<number>): boolean;
5
- /** Manifest text with the root `targets` map removed, so its URLs are declarations. */
6
- export declare function contentOutsideTargets(content: string): string;
7
4
  export declare function collectWorkspaceSlotIds(files: readonly BindingSourceFile[]): string[];
5
+ /** These are schema-defined capability maps, not arbitrary keys containing "workspace". */
8
6
  export declare function workspaceSlotIds(file: BindingSourceFile): string[];
9
- export declare function scannable(file: BindingSourceFile): boolean;
10
7
  export declare function basename(path: string): string;
@@ -0,0 +1,56 @@
1
+ import yaml from 'js-yaml';
2
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3
+ /** Only schema-defined artifact manifest fields declare bindings; source text is never inspected. */
4
+ function manifestDocument(file) {
5
+ if (file.encoding === 'base64' || basename(file.path) !== 'artifact.bundle.yml')
6
+ return undefined;
7
+ return object(yaml.load(file.content));
8
+ }
9
+ function object(value) {
10
+ return isRecord(value) ? value : undefined;
11
+ }
12
+ function isRecord(value) {
13
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
14
+ }
15
+ export function originsDeclaredInFiles(files) {
16
+ const origins = [];
17
+ for (const file of files) {
18
+ if (!manifestDocument(file))
19
+ continue;
20
+ for (const [name, url] of Object.entries(declaredTargets(file.content))) {
21
+ try {
22
+ origins.push({ name, origin: new URL(url).origin.toLowerCase() });
23
+ }
24
+ catch {
25
+ // An invalid selected target is reported by the binding validator.
26
+ }
27
+ }
28
+ }
29
+ return origins;
30
+ }
31
+ export function declaredTargets(content) {
32
+ const targets = object(object(yaml.load(content))?.targets);
33
+ const urls = {};
34
+ for (const [name, value] of Object.entries(targets ?? {})) {
35
+ const url = object(value)?.url;
36
+ if (typeof url === 'string')
37
+ urls[name] = url;
38
+ }
39
+ return urls;
40
+ }
41
+ export function collectWorkspaceSlotIds(files) {
42
+ return [...new Set(files.flatMap(workspaceSlotIds))];
43
+ }
44
+ /** These are schema-defined capability maps, not arbitrary keys containing "workspace". */
45
+ export function workspaceSlotIds(file) {
46
+ const manifest = manifestDocument(file);
47
+ if (!manifest)
48
+ return [];
49
+ const browser = object(object(manifest.capabilities)?.storage);
50
+ const server = object(object(object(manifest.server)?.capabilities)?.storage);
51
+ return [...new Set([...Object.keys(browser ?? {}), ...Object.keys(server ?? {})]
52
+ .filter((id) => UUID.test(id)).map((id) => id.toLowerCase()))];
53
+ }
54
+ export function basename(path) {
55
+ return path.split('/').at(-1) ?? path;
56
+ }
@@ -1,4 +1,4 @@
1
- import { basename, collectWorkspaceSlotIds, contentContainsOrigin, declaredTargets, contentOutsideTargets, originsDeclaredInFiles, scannable, workspaceSlotIds, } from './env-binding-scan.js';
1
+ import { basename, collectWorkspaceSlotIds, declaredTargets, originsDeclaredInFiles, workspaceSlotIds, } from './env-binding-manifest.js';
2
2
  export class CrossEnvironmentBindingError extends Error {
3
3
  deployEnvironment;
4
4
  bindings;
@@ -19,7 +19,7 @@ export function canonicalDeploymentOrigin(url) {
19
19
  }
20
20
  export function formatCrossEnvironmentBindings({ deployEnvironment, bindings, }) {
21
21
  const lines = bindings.map((binding) => ` ${binding.path}: ${binding.message}`);
22
- return `Refusing to deploy to ${deployEnvironment} because the source binds another environment.\n${lines.join('\n')}`;
22
+ return `Refusing to deploy to ${deployEnvironment} because declared manifest bindings do not match the selected environment.\n${lines.join('\n')}`;
23
23
  }
24
24
  export function findCrossEnvironmentBindings(input) {
25
25
  const deployOrigin = canonicalDeploymentOrigin(input.deployOrigin);
@@ -35,7 +35,6 @@ export function findCrossEnvironmentBindings(input) {
35
35
  const present = workspaceIdsPresent(input);
36
36
  return dedupe([
37
37
  ...selectedTargetFindings({ ...input, deployOrigin, origins }),
38
- ...foreignOriginFindings({ ...input, deployOrigin, origins }),
39
38
  ...unresolvedWorkspaceFindings(input),
40
39
  ...foreignWorkspaceFindings({ ...input, present }),
41
40
  ]);
@@ -93,35 +92,6 @@ function selectedTargetFindings({ deployEnvironment, deployOrigin, origins, file
93
92
  }
94
93
  return findings;
95
94
  }
96
- function foreignOriginFindings({ deployOrigin, origins, files, }) {
97
- const foreign = new Map();
98
- for (const origin of origins) {
99
- if (origin.origin === deployOrigin)
100
- continue;
101
- const names = foreign.get(origin.origin) ?? [];
102
- if (!names.includes(origin.name))
103
- names.push(origin.name);
104
- foreign.set(origin.origin, names);
105
- }
106
- const findings = [];
107
- for (const file of files) {
108
- if (!scannable(file))
109
- continue;
110
- const content = basename(file.path) === 'artifact.bundle.yml' ? contentOutsideTargets(file.content) : file.content;
111
- for (const [origin, names] of foreign) {
112
- if (!contentContainsOrigin(content, origin, new Set()))
113
- continue;
114
- findings.push({
115
- path: file.path,
116
- kind: 'api_origin',
117
- value: origin,
118
- environment: names[0],
119
- message: `API origin ${origin} belongs to ${names.join(', ')}`,
120
- });
121
- }
122
- }
123
- return findings;
124
- }
125
95
  function unresolvedWorkspaceFindings(input) {
126
96
  const seen = new Set();
127
97
  const findings = [];
@@ -142,8 +112,6 @@ function foreignWorkspaceFindings({ deployEnvironment, files, present, }) {
142
112
  const findings = [];
143
113
  const reported = new Set();
144
114
  for (const file of files) {
145
- if (!scannable(file))
146
- continue;
147
115
  for (const workspaceId of workspaceSlotIds(file)) {
148
116
  if (present.has(workspaceId) || reported.has(`${file.path}:${workspaceId}`))
149
117
  continue;
@@ -1,3 +1,14 @@
1
+ /** Read-only server provenance preflight, never a permission grant or an activation token. */
2
+ export declare function checkRepositoryDeploymentIdentity({ baseUrl, token, repoId, commit }: {
3
+ baseUrl: string;
4
+ token: string;
5
+ repoId: string;
6
+ commit: string;
7
+ }): Promise<{
8
+ tenantId: string;
9
+ repositoryId: string;
10
+ generation: number;
11
+ }>;
1
12
  /** Resolve `namespace/name` to a repo id + default branch (FGA-filtered list, paginated). */
2
13
  export declare function resolveRepo({ baseUrl, token, namespace, name, }: {
3
14
  baseUrl: string;
@@ -1,6 +1,16 @@
1
1
  import { lstat, mkdir, realpath, unlink, writeFile } from 'node:fs/promises';
2
2
  import { dirname, resolve, sep } from 'node:path';
3
3
  import { getJson, getJsonOr404 } from './api.js';
4
+ import { RepositoryIdentitySchema } from './repository-identity.js';
5
+ /** Read-only server provenance preflight, never a permission grant or an activation token. */
6
+ export async function checkRepositoryDeploymentIdentity({ baseUrl, token, repoId, commit }) {
7
+ const result = await getJson({ baseUrl, token,
8
+ path: `/api/git-service/repos/${encodeURIComponent(repoId)}/deployment-admission?commit=${encodeURIComponent(commit)}` });
9
+ const identity = RepositoryIdentitySchema.parse(result.identity);
10
+ if (identity.repositoryId !== repoId || result.commit !== commit)
11
+ throw new Error('Deployment preflight returned a different repository or commit');
12
+ return identity;
13
+ }
4
14
  const REPO_PAGE_SIZE = 200;
5
15
  /**
6
16
  * Encode a `/`-delimited git path for a `[...path]` / `[...ref]` catch-all
@@ -86,6 +86,9 @@ export declare const artifactNextBuildConfigSchema: z.ZodPrefault<z.ZodObject<{
86
86
  url: z.ZodString;
87
87
  sha256: z.ZodString;
88
88
  }, z.core.$strict>>>;
89
+ source_includes: z.ZodDefault<z.ZodArray<z.ZodObject<{
90
+ path: z.ZodString;
91
+ }, z.core.$strict>>>;
89
92
  }, z.core.$strict>>;
90
93
  export declare const artifactNextServerSchema: z.ZodPrefault<z.ZodObject<{
91
94
  capabilities: z.ZodPrefault<z.ZodObject<{
@@ -280,6 +283,9 @@ export declare const artifactStudioNextManifestSchema: z.ZodObject<{
280
283
  url: z.ZodString;
281
284
  sha256: z.ZodString;
282
285
  }, z.core.$strict>>>;
286
+ source_includes: z.ZodDefault<z.ZodArray<z.ZodObject<{
287
+ path: z.ZodString;
288
+ }, z.core.$strict>>>;
283
289
  }, z.core.$strict>>;
284
290
  server: z.ZodPrefault<z.ZodObject<{
285
291
  capabilities: z.ZodPrefault<z.ZodObject<{
@@ -1,5 +1,6 @@
1
1
  import { ontologyContractSchema } from '@sequenceholdings/orm/canonical';
2
2
  import { z } from 'zod';
3
+ import { artifactNextSourceIncludesSchema } from './next-source-includes.js';
3
4
  import { capabilityPinsSchema, customRolesBlockSchema, validateCapabilityPins } from './manifest-custom-roles.js';
4
5
  import { artifactNextBrowserDataSchema, validateBrowserDataAdmission } from './manifest-next-browser-data.js';
5
6
  import { isManagedServerInvokePath } from './server/managed-server-path.js';
@@ -77,6 +78,8 @@ export const artifactNextPublicFontSchema = z.strictObject({
77
78
  export const artifactNextBuildConfigSchema = z.strictObject({
78
79
  public_env: z.record(publicEnvironmentName, z.string().max(8192)).refine((values) => Object.keys(values).length <= 64, 'At most 64 public build variables are allowed').default({}),
79
80
  public_fonts: z.array(artifactNextPublicFontSchema).max(8).refine((fonts) => new Set(fonts.map((font) => font.path)).size === fonts.length, 'Public font destination paths must be unique').default([]),
81
+ /** Repository directories compiled alongside this bundle; see next-source-includes.ts. */
82
+ source_includes: artifactNextSourceIncludesSchema,
80
83
  }).prefault({});
81
84
  const hostname = z.string().max(253).regex(/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/, 'Egress entries must be exact lowercase DNS hostnames without schemes, ports, paths, IPs, or wildcards').refine((host) => !host.endsWith('.localhost') && !host.endsWith('.local') && !host.endsWith('.internal'), 'Egress cannot target local or platform-internal hostnames');
82
85
  const managedServerInvokePath = z.string().refine(isManagedServerInvokePath, 'Lattice accept paths are exact origin-relative paths with no query or traversal');
@@ -3,6 +3,7 @@ import { type ArtifactStudioStaticManifest } from './manifest-static.js';
3
3
  import { type ArtifactStudioNextManifest } from './manifest-next.js';
4
4
  export * from './manifest-static.js';
5
5
  export * from './manifest-next.js';
6
+ export * from './next-source-includes.js';
6
7
  /** Versioned parsing keeps schema-v1 stripping/default behavior unchanged. */
7
8
  export declare const artifactStudioManifestSchema: z.ZodUnion<readonly [z.ZodObject<{
8
9
  ontology: z.ZodOptional<z.ZodObject<{
@@ -161,6 +162,9 @@ export declare const artifactStudioManifestSchema: z.ZodUnion<readonly [z.ZodObj
161
162
  url: z.ZodString;
162
163
  sha256: z.ZodString;
163
164
  }, z.core.$strict>>>;
165
+ source_includes: z.ZodDefault<z.ZodArray<z.ZodObject<{
166
+ path: z.ZodString;
167
+ }, z.core.$strict>>>;
164
168
  }, z.core.$strict>>;
165
169
  server: z.ZodPrefault<z.ZodObject<{
166
170
  capabilities: z.ZodPrefault<z.ZodObject<{
package/dist/manifest.js CHANGED
@@ -3,6 +3,7 @@ import { artifactStudioStaticManifestSchema } from './manifest-static.js';
3
3
  import { artifactStudioNextManifestSchema } from './manifest-next.js';
4
4
  export * from './manifest-static.js';
5
5
  export * from './manifest-next.js';
6
+ export * from './next-source-includes.js';
6
7
  /** Versioned parsing keeps schema-v1 stripping/default behavior unchanged. */
7
8
  export const artifactStudioManifestSchema = z.union([
8
9
  artifactStudioStaticManifestSchema,
@@ -37,7 +37,21 @@ export interface NextArtifactBridge {
37
37
  reportRuntimeLog(input: NextArtifactRuntimeLog): void;
38
38
  forwardHostShortcut(input: Pick<KeyboardEvent, 'key' | 'code' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey'>): boolean;
39
39
  host: SequenceHost;
40
- lattice: SequenceLattice;
40
+ /** Read only the current viewer's app navigation decisions. This is not an authorization grant. */
41
+ auth: {
42
+ getAppAccess(): Promise<Record<string, boolean>>;
43
+ };
44
+ lattice: SequenceLattice & {
45
+ /** Ask the host to verify its own task completed after an app-owned mutation.
46
+ * Returns false while the native step is pending; retry this read, never the mutation. */
47
+ notifyAdvance(): Promise<{
48
+ notified: boolean;
49
+ }>;
50
+ /** Verify current host task admission without notifying or completing it. */
51
+ prepareAdvance(): Promise<{
52
+ supported: true;
53
+ }>;
54
+ };
41
55
  }
42
56
  /**
43
57
  * An SSR-safe external store and DOM transport. Call connect from a client effect,
@@ -1,3 +1,4 @@
1
+ import { z } from 'zod';
1
2
  import { createNextArtifactBridgeEnvelope, EMPTY_NEXT_ARTIFACT_LATTICE_CONTEXT, isNextArtifactRelativePath, nextArtifactInitialContextSchema, nextArtifactThemeSchema, parseNextArtifactBridgeEnvelope, validateNextArtifactBootstrap, } from './protocol.js';
2
3
  import { createNextArtifactRequests } from './requests.js';
3
4
  import { createNextArtifactStreams } from './streams.js';
@@ -203,7 +204,23 @@ export function createNextArtifactBridge(options) {
203
204
  send({ type: 'artifact-studio-host-shortcut', key: input.key, code: input.code, metaKey: input.metaKey, ctrlKey: input.ctrlKey, shiftKey: input.shiftKey, altKey: input.altKey });
204
205
  return true;
205
206
  },
206
- lattice: { context: () => ({ ...(snapshot.lattice ?? EMPTY_NEXT_ARTIFACT_LATTICE_CONTEXT) }) },
207
+ auth: {
208
+ async getAppAccess() {
209
+ const result = await bridge.request({ method: 'auth.getAppAccess' });
210
+ return z.record(z.string(), z.boolean()).parse(result);
211
+ },
212
+ },
213
+ lattice: {
214
+ context: () => ({ ...(snapshot.lattice ?? EMPTY_NEXT_ARTIFACT_LATTICE_CONTEXT) }),
215
+ async prepareAdvance() {
216
+ const result = await bridge.request({ method: 'lattice.prepareAdvance' });
217
+ return z.object({ supported: z.literal(true) }).strict().parse(result);
218
+ },
219
+ async notifyAdvance() {
220
+ const result = await bridge.request({ method: 'lattice.notifyAdvance' });
221
+ return z.object({ notified: z.boolean() }).strict().parse(result);
222
+ },
223
+ },
207
224
  host: {
208
225
  navigate(path) {
209
226
  requireReady();
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Source includes: repository directories a Next artifact compiles alongside
3
+ * its own bundle, such as a generated ORM client several apps share.
4
+ *
5
+ * The bundle directory stays the build boundary. An include is read from the
6
+ * same immutable commit as the bundle, with the same blob, exclusion and size
7
+ * checks, and placed at a fixed mount inside the app root, so it becomes
8
+ * ordinary bundle source: hashed into the source hash, compiled from within
9
+ * the app (no external directories), and invisible to anything not declared.
10
+ * Apps import it through a tsconfig path alias to the mount; local
11
+ * development writes the same mount. Never accepted from a local upload,
12
+ * whose bytes outside the bundle no dirty bit covers.
13
+ */
14
+ import { z } from 'zod';
15
+ /** Where includes are placed, relative to app_root. Reserved when any include is declared. */
16
+ export declare const SOURCE_INCLUDE_MOUNT = ".artifact-includes";
17
+ export declare const MAX_SOURCE_INCLUDES = 4;
18
+ export declare const artifactNextSourceIncludeSchema: z.ZodObject<{
19
+ path: z.ZodString;
20
+ }, z.core.$strict>;
21
+ export type ArtifactNextSourceInclude = z.infer<typeof artifactNextSourceIncludeSchema>;
22
+ export declare const artifactNextSourceIncludesSchema: z.ZodDefault<z.ZodArray<z.ZodObject<{
23
+ path: z.ZodString;
24
+ }, z.core.$strict>>>;
25
+ export declare function sourceIncludeMountRoot({ appRoot }: {
26
+ appRoot: string;
27
+ }): string;
28
+ export interface PlannedSourceInclude {
29
+ /** Path in the repository tree at the build commit. */
30
+ readonly repositoryPath: string;
31
+ /** Path in the bundle's source snapshot, under the reserved mount. */
32
+ readonly sourcePath: string;
33
+ }
34
+ /**
35
+ * Which repository files each include contributes and where they mount.
36
+ * `paths` are repository-relative file paths at the build commit; `sourceRoot`
37
+ * is the bundle directory. Throws for an include that overlaps the bundle or
38
+ * contributes nothing.
39
+ */
40
+ export declare function planSourceIncludes({ includes, sourceRoot, appRoot, paths }: {
41
+ includes: readonly ArtifactNextSourceInclude[];
42
+ sourceRoot: string;
43
+ appRoot: string;
44
+ paths: readonly string[];
45
+ }): PlannedSourceInclude[];
46
+ /** A bundle that declares includes may not commit its own files under the mount they own. */
47
+ export declare function assertNoReservedSourcePaths({ appRoot, paths }: {
48
+ appRoot: string;
49
+ paths: readonly string[];
50
+ }): void;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Source includes: repository directories a Next artifact compiles alongside
3
+ * its own bundle, such as a generated ORM client several apps share.
4
+ *
5
+ * The bundle directory stays the build boundary. An include is read from the
6
+ * same immutable commit as the bundle, with the same blob, exclusion and size
7
+ * checks, and placed at a fixed mount inside the app root, so it becomes
8
+ * ordinary bundle source: hashed into the source hash, compiled from within
9
+ * the app (no external directories), and invisible to anything not declared.
10
+ * Apps import it through a tsconfig path alias to the mount; local
11
+ * development writes the same mount. Never accepted from a local upload,
12
+ * whose bytes outside the bundle no dirty bit covers.
13
+ */
14
+ import { z } from 'zod';
15
+ import { isExcludedArtifactSourcePath } from './source-exclusions.js';
16
+ /** Where includes are placed, relative to app_root. Reserved when any include is declared. */
17
+ export const SOURCE_INCLUDE_MOUNT = '.artifact-includes';
18
+ export const MAX_SOURCE_INCLUDES = 4;
19
+ const includePath = z.string().min(1).max(512).refine((value) => !value.startsWith('/') && !/[\\:\s\x00-\x1f]/.test(value) &&
20
+ value.split('/').every((part) => part !== '' && part !== '.' && part !== '..' && !part.startsWith('.') && part !== 'node_modules') &&
21
+ !isExcludedArtifactSourcePath({ path: value }), 'Source include paths must be repository-relative directories without traversal, hidden directories, or excluded segments');
22
+ export const artifactNextSourceIncludeSchema = z.strictObject({ path: includePath });
23
+ const isWithin = ({ path, root }) => path === root || path.startsWith(`${root}/`);
24
+ export const artifactNextSourceIncludesSchema = z.array(artifactNextSourceIncludeSchema).max(MAX_SOURCE_INCLUDES).superRefine((includes, ctx) => {
25
+ includes.forEach((include, index) => {
26
+ const overlap = includes.findIndex((other, otherIndex) => otherIndex !== index && isWithin({ path: include.path, root: other.path }));
27
+ if (overlap !== -1)
28
+ ctx.addIssue({ code: 'custom', path: [index, 'path'], message: `${include.path} overlaps ${includes[overlap]?.path}; declare each directory once` });
29
+ });
30
+ }).default([]);
31
+ export function sourceIncludeMountRoot({ appRoot }) {
32
+ return appRoot === '.' ? SOURCE_INCLUDE_MOUNT : `${appRoot}/${SOURCE_INCLUDE_MOUNT}`;
33
+ }
34
+ /**
35
+ * Which repository files each include contributes and where they mount.
36
+ * `paths` are repository-relative file paths at the build commit; `sourceRoot`
37
+ * is the bundle directory. Throws for an include that overlaps the bundle or
38
+ * contributes nothing.
39
+ */
40
+ export function planSourceIncludes({ includes, sourceRoot, appRoot, paths }) {
41
+ if (includes.length === 0)
42
+ return [];
43
+ if (sourceRoot === '.')
44
+ throw new Error('Source includes are unavailable to a bundle at the repository root; its whole tree is already the bundle');
45
+ const mount = sourceIncludeMountRoot({ appRoot });
46
+ const planned = [];
47
+ for (const { path: root } of includes) {
48
+ if (isWithin({ path: root, root: sourceRoot }))
49
+ throw new Error(`Source include ${root} is already inside the bundle ${sourceRoot}`);
50
+ if (isWithin({ path: sourceRoot, root }))
51
+ throw new Error(`Source include ${root} contains the bundle ${sourceRoot}`);
52
+ const files = paths
53
+ .filter((path) => path.startsWith(`${root}/`) && !isExcludedArtifactSourcePath({ path: path.slice(root.length + 1) }))
54
+ .sort();
55
+ if (files.length === 0)
56
+ throw new Error(`Source include ${root} contains no source files at this commit`);
57
+ for (const repositoryPath of files)
58
+ planned.push({ repositoryPath, sourcePath: `${mount}/${repositoryPath}` });
59
+ }
60
+ return planned;
61
+ }
62
+ /** A bundle that declares includes may not commit its own files under the mount they own. */
63
+ export function assertNoReservedSourcePaths({ appRoot, paths }) {
64
+ const mount = sourceIncludeMountRoot({ appRoot });
65
+ const reserved = paths.find((path) => isWithin({ path, root: mount }));
66
+ if (reserved)
67
+ throw new Error(`${reserved} is under ${mount}, which is reserved for source includes; remove it from the commit (keep the local copy ignored)`);
68
+ }
@@ -14,14 +14,22 @@ export declare function extractChainguardAuthLines(userNpmrc: string): string[];
14
14
  /** Merge trusted Chainguard auth from ~/.npmrc with pinned install settings. */
15
15
  export declare function buildControlledArtifactNpmrc(userNpmrcPath?: string): string;
16
16
  export interface PrepareBuildOptions {
17
- /** Remote sources (--repo / --git-url) install when package.json exists. */
17
+ /** Remote sources (--repo / --git-url) install the production dependencies package.json declares. */
18
18
  remote?: boolean;
19
+ /**
20
+ * Set by an embedder whose host cannot reach a package registry, such as an
21
+ * agent sandbox. A remote source that needs an install fails with this
22
+ * guidance instead of attempting one.
23
+ */
24
+ installUnavailable?: string;
19
25
  /** Parent environment override for deterministic security tests. */
20
26
  sourceEnv?: NodeJS.ProcessEnv;
21
27
  }
22
28
  /**
23
29
  * Whether the build should run `pnpm install` before Vite bundles the tree.
24
- * Only remote materializations (--repo / --git-url) are bare and need install.
30
+ * Only remote materializations (--repo / --git-url) are bare, and only
31
+ * production dependencies need installing: the build aliases every scaffold
32
+ * import (react, atlas-ui, react-query, the SDK) to this package's pinned copies.
25
33
  */
26
34
  export declare function shouldInstallArtifactDependencies(dir: string, opts?: PrepareBuildOptions): boolean;
27
35
  /**
@@ -46,14 +46,35 @@ export function buildControlledArtifactNpmrc(userNpmrcPath = join(homedir(), '.n
46
46
  }
47
47
  return `${authBlock}${ARTIFACT_BUILD_NPMRC}`;
48
48
  }
49
+ /** The package.json fields `pnpm install --prod` installs. devDependencies never reach a build. */
50
+ const INSTALLED_DEPENDENCY_FIELDS = ['dependencies', 'optionalDependencies'];
51
+ function declaresInstalledDependencies(packageJsonPath) {
52
+ let manifest;
53
+ try {
54
+ manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
55
+ }
56
+ catch (error) {
57
+ throw new Error(`package.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
58
+ }
59
+ if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest)) {
60
+ throw new Error('package.json must be a JSON object.');
61
+ }
62
+ return INSTALLED_DEPENDENCY_FIELDS.some((field) => {
63
+ const value = Reflect.get(manifest, field);
64
+ return typeof value === 'object' && value !== null && Object.keys(value).length > 0;
65
+ });
66
+ }
49
67
  /**
50
68
  * Whether the build should run `pnpm install` before Vite bundles the tree.
51
- * Only remote materializations (--repo / --git-url) are bare and need install.
69
+ * Only remote materializations (--repo / --git-url) are bare, and only
70
+ * production dependencies need installing: the build aliases every scaffold
71
+ * import (react, atlas-ui, react-query, the SDK) to this package's pinned copies.
52
72
  */
53
73
  export function shouldInstallArtifactDependencies(dir, opts = {}) {
54
74
  if (!opts.remote)
55
75
  return false;
56
- return existsSync(join(dir, 'package.json'));
76
+ const packageJsonPath = join(dir, 'package.json');
77
+ return existsSync(packageJsonPath) && declaresInstalledDependencies(packageJsonPath);
57
78
  }
58
79
  /**
59
80
  * Install artifact runtime deps into the build root. Platform peers (react,
@@ -61,8 +82,17 @@ export function shouldInstallArtifactDependencies(dir, opts = {}) {
61
82
  * time — this step resolves app-specific imports like recharts or reactflow.
62
83
  */
63
84
  export async function prepareArtifactBuildRoot(dir, opts = {}) {
64
- if (!shouldInstallArtifactDependencies(dir, opts))
85
+ if (!opts.remote)
86
+ return;
87
+ if (!shouldInstallArtifactDependencies(dir, opts)) {
88
+ // Nothing to install, but a node_modules committed into the checkout must
89
+ // still never resolve imports for the build (see the install path below).
90
+ await rm(join(dir, 'node_modules'), { recursive: true, force: true });
65
91
  return;
92
+ }
93
+ if (opts.installUnavailable) {
94
+ throw new Error(`This artifact declares production dependencies (package.json dependencies or optionalDependencies), which install from the package registry. ${opts.installUnavailable}`);
95
+ }
66
96
  if (!existsSync(join(dir, 'pnpm-lock.yaml'))) {
67
97
  throw new Error('Remote artifact deploy requires pnpm-lock.yaml — run `pnpm install` locally and commit the lockfile.');
68
98
  }
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ /** Internal wire contract shared by Atlas and seq-studio. Not deployment authorization. */
3
+ export declare const REPOSITORY_IDENTITY_PATH = ".seq/identity.json";
4
+ export declare const RepositoryIdentitySchema: z.ZodObject<{
5
+ tenantId: z.ZodString;
6
+ repositoryId: z.ZodString;
7
+ generation: z.ZodNumber;
8
+ }, z.core.$strict>;
9
+ export type RepositoryIdentity = z.infer<typeof RepositoryIdentitySchema>;
10
+ export declare const RepositoryReceiptSchema: z.ZodObject<{
11
+ schemaVersion: z.ZodLiteral<1>;
12
+ identity: z.ZodObject<{
13
+ tenantId: z.ZodString;
14
+ repositoryId: z.ZodString;
15
+ generation: z.ZodNumber;
16
+ }, z.core.$strict>;
17
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodJSONSchema>>;
18
+ }, z.core.$strict>;
19
+ export type RepositoryReceipt = z.infer<typeof RepositoryReceiptSchema>;
20
+ export declare function parseRepositoryReceipt({ text }: {
21
+ text: string;
22
+ }): RepositoryReceipt;
23
+ export declare function renderRepositoryReceipt({ identity, metadata }: {
24
+ identity: RepositoryIdentity;
25
+ metadata?: RepositoryReceipt['metadata'];
26
+ }): string;
27
+ export declare function assertRepositoryIdentity({ receipt, expected }: {
28
+ receipt: RepositoryReceipt;
29
+ /** Must come from the authenticated platform record, never from the receipt itself. */
30
+ expected: RepositoryIdentity;
31
+ }): void;