@sequenceholdings/artifact-studio 0.9.0 → 0.9.2

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.
@@ -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;
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod';
2
+ /** Internal wire contract shared by Atlas and seq-studio. Not deployment authorization. */
3
+ export const REPOSITORY_IDENTITY_PATH = '.seq/identity.json';
4
+ export const RepositoryIdentitySchema = z.object({
5
+ tenantId: z.string().max(32).regex(/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/),
6
+ repositoryId: z.string().uuid(),
7
+ generation: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
8
+ }).strict();
9
+ export const RepositoryReceiptSchema = z.object({
10
+ schemaVersion: z.literal(1),
11
+ identity: RepositoryIdentitySchema,
12
+ // Extensible, non-authoritative annotations. Never use metadata for auth or routing.
13
+ metadata: z.record(z.string(), z.json()).default({}),
14
+ }).strict();
15
+ export function parseRepositoryReceipt({ text }) {
16
+ if (new TextEncoder().encode(text).byteLength > 65_536)
17
+ throw new Error('Repository identity exceeds 64 KiB');
18
+ const value = JSON.parse(text);
19
+ const visit = (entry, depth) => {
20
+ if (depth > 32)
21
+ throw new Error('Repository identity exceeds the metadata depth limit');
22
+ if (entry !== null && typeof entry === 'object') {
23
+ for (const child of Object.values(entry))
24
+ visit(child, depth + 1);
25
+ }
26
+ };
27
+ visit(value, 0);
28
+ return RepositoryReceiptSchema.parse(value);
29
+ }
30
+ export function renderRepositoryReceipt({ identity, metadata = {} }) {
31
+ const text = JSON.stringify({ schemaVersion: 1, identity, metadata }, null, 2) + '\n';
32
+ parseRepositoryReceipt({ text });
33
+ return text;
34
+ }
35
+ export function assertRepositoryIdentity({ receipt, expected }) {
36
+ if (receipt.identity.tenantId !== expected.tenantId ||
37
+ receipt.identity.repositoryId !== expected.repositoryId ||
38
+ receipt.identity.generation !== expected.generation) {
39
+ throw new Error('Repository identity does not match the platform record; do not edit identity fields to change ownership');
40
+ }
41
+ }
@@ -17,10 +17,7 @@ export declare const ARTIFACT_NEXT_SOURCE_LIMITS: Readonly<{
17
17
  export declare const artifactSha256DigestSchema: z.ZodString;
18
18
  /** Immutable Next deploymentId from compiled output; distinct from the Atlas activation ID. */
19
19
  export declare const artifactNextFrameworkBuildIdSchema: z.ZodString;
20
- /** Shared upload exclusion policy. Content scanning and filesystem containment remain separate checks. */
21
- export declare function isExcludedArtifactSourcePath({ path }: {
22
- path: string;
23
- }): boolean;
20
+ export { isExcludedArtifactSourcePath } from './source-exclusions.js';
24
21
  /** POSIX source paths only; filesystem readers must additionally reject symlink escapes. */
25
22
  export declare const artifactSourcePathSchema: z.ZodString;
26
23
  /** An exact browser origin, not a URL prefix or an allowlist wildcard. */
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { ARTIFACT_NEXT_RUNTIME, artifactNextSecretNameSchema } from './manifest-next.js';
3
3
  import { MAX_STUDIO_PATH_LENGTH } from './paths.js';
4
4
  import { isLoopbackHostname } from './loopback.js';
5
+ import { isExcludedArtifactSourcePath } from './source-exclusions.js';
5
6
  export { resolveArtifactNextImages } from './next-image-config.js';
6
7
  /** Atlas routes the gateway calls; every one authenticates the gateway itself. */
7
8
  export const ARTIFACT_NEXT_CONTROL_PATH = '/api/artifact-studio/next/control';
@@ -27,21 +28,7 @@ export const artifactSha256DigestSchema = z.string().regex(/^[a-f0-9]{64}$/, 'Ex
27
28
  export const artifactNextFrameworkBuildIdSchema = z.string().min(1).max(128).refine((value) => !/[^A-Za-z0-9_-]/.test(value), 'Expected a canonical Next.js deploymentId containing only letters, digits, underscores and hyphens');
28
29
  const imageDigestSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/, 'Expected a SHA-256 image digest');
29
30
  const imageReferenceSchema = z.string().max(1024).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*(?::[1-9][0-9]{0,4})?(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)+@sha256:[a-f0-9]{64}$/, 'Expected a registry image reference pinned by digest');
30
- const excludedSourceSegments = new Set([
31
- '.git', 'node_modules', '.next', '.npmrc', '.netrc', '.aws', '.ssh', '.config',
32
- 'dist', '.artifact-studio', '.git-credentials', '.pypirc', '.azure', '.gcloud', '.doppler',
33
- '.turbo', 'coverage', '.docker', '.seq-next-output.json',
34
- ]);
35
- /** Shared upload exclusion policy. Content scanning and filesystem containment remain separate checks. */
36
- export function isExcludedArtifactSourcePath({ path }) {
37
- return path.split('/').some((segment) => {
38
- const normalized = segment.toLowerCase();
39
- return excludedSourceSegments.has(normalized)
40
- || normalized.startsWith('.env') || normalized.startsWith('.yarnrc')
41
- || /(?:^|[-_.])(?:credentials?|service[-_]?account)(?:[-_.].*)?\.json$/.test(normalized)
42
- || /\.(pem|key|p12|pfx|jks)$/.test(normalized);
43
- });
44
- }
31
+ export { isExcludedArtifactSourcePath } from './source-exclusions.js';
45
32
  /** POSIX source paths only; filesystem readers must additionally reject symlink escapes. */
46
33
  export const artifactSourcePathSchema = z.string().min(1).max(MAX_STUDIO_PATH_LENGTH).refine((path) => {
47
34
  if (path !== path.trim() || /[\\%:\u0000-\u001f\u007f]/.test(path))
@@ -4,16 +4,37 @@ export interface ArtifactSourceBoundaryOptions {
4
4
  trustedExternalRoots: readonly string[];
5
5
  }
6
6
  /**
7
- * Artifact source must be self-contained: allowing a relative import to escape
8
- * the build root would let uncommitted files elsewhere in a containing
9
- * checkout enter the official bundle without being covered by its dirty bit.
10
- * Published dependencies and the CLI-pinned platform peers are intentionally
11
- * allowed because they are external to the artifact by design.
7
+ * Which files may feed an artifact build. Artifact source must be
8
+ * self-contained: a relative path that escapes the build root would let files
9
+ * elsewhere on the build machine (uncommitted checkout files, or the caller's
10
+ * credentials) enter the official bundle. Published dependencies and the
11
+ * CLI-pinned platform peers are intentionally allowed because they are external
12
+ * to the artifact by design.
12
13
  */
13
- export declare function rejectOutsideSourcePlugin({ root, trustedExternalRoots, }: ArtifactSourceBoundaryOptions): Plugin;
14
+ export interface SourceBoundary {
15
+ /** Physical source root. */
16
+ root: string;
17
+ /**
18
+ * Throw unless `filePath` may enter the build when `importer` references it.
19
+ * `bareSpecifier` marks a package reference rather than a path. Paths are
20
+ * compared physically, so an in-root symlink cannot admit an outside file.
21
+ */
22
+ assertBuildInput(input: {
23
+ filePath: string;
24
+ importer?: string;
25
+ bareSpecifier: boolean;
26
+ reference: string;
27
+ }): void;
28
+ /** Whether `filePath` is part of the CLI-pinned platform toolchain. */
29
+ isTrusted(filePath: string): boolean;
30
+ }
31
+ export declare function createSourceBoundary({ root, trustedExternalRoots }: ArtifactSourceBoundaryOptions): SourceBoundary;
32
+ /** Reject module imports that resolve outside the source boundary. */
33
+ export declare function rejectOutsideSourcePlugin(boundary: SourceBoundary): Plugin;
14
34
  /**
15
35
  * Whether `filePath` is inside a `node_modules` directory. Bundlers pass
16
36
  * Windows paths with forward slashes, so compare in that form whatever the
17
37
  * platform separator is.
18
38
  */
19
39
  export declare function isNodeModulesPath(filePath: string): boolean;
40
+ export declare function isBareImport(source: string): boolean;
@@ -1,15 +1,46 @@
1
1
  import { realpathSync } from 'node:fs';
2
- import { isAbsolute, relative, resolve, sep } from 'node:path';
3
- /**
4
- * Artifact source must be self-contained: allowing a relative import to escape
5
- * the build root would let uncommitted files elsewhere in a containing
6
- * checkout enter the official bundle without being covered by its dirty bit.
7
- * Published dependencies and the CLI-pinned platform peers are intentionally
8
- * allowed because they are external to the artifact by design.
9
- */
10
- export function rejectOutsideSourcePlugin({ root, trustedExternalRoots, }) {
2
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ export function createSourceBoundary({ root, trustedExternalRoots }) {
11
4
  const rootResolved = realpathSync(resolve(root));
12
5
  const trustedRoots = trustedExternalRoots.map((trustedRoot) => realpathSync(resolve(trustedRoot)));
6
+ const isTrusted = (filePath) => isTrustedExternalPath(physicalPath(filePath), trustedRoots);
7
+ return {
8
+ root: rootResolved,
9
+ isTrusted,
10
+ assertBuildInput({ filePath, importer, bareSpecifier, reference }) {
11
+ const realFilePath = physicalPath(filePath);
12
+ if (isPathWithin(rootResolved, realFilePath))
13
+ return;
14
+ const importerPath = importer?.split('?')[0];
15
+ const importerIsTrustedExternal = importerPath != null &&
16
+ (isNodeModulesPath(importerPath) || (isAbsolute(importerPath) && isTrusted(importerPath)));
17
+ if (isNodeModulesPath(filePath) && (bareSpecifier || importerIsTrustedExternal))
18
+ return;
19
+ if (isTrustedExternalPath(realFilePath, trustedRoots))
20
+ return;
21
+ const from = importerPath ? ` (referenced from ${relative(rootResolved, importerPath)})` : '';
22
+ throw new Error(`Artifact ${reference} "${filePath}" resolves outside artifact source directory ` +
23
+ `"${rootResolved}"${from}. Keep relative build inputs inside the artifact ` +
24
+ 'source directory or publish shared code as a dependency.');
25
+ },
26
+ };
27
+ }
28
+ /**
29
+ * The realpath of `filePath`, or of its deepest existing ancestor joined with
30
+ * the rest, so a reference to a file that does not exist yet is still compared
31
+ * physically.
32
+ */
33
+ function physicalPath(filePath) {
34
+ try {
35
+ return realpathSync(filePath);
36
+ }
37
+ catch {
38
+ const parent = dirname(filePath);
39
+ return parent === filePath ? filePath : join(physicalPath(parent), basename(filePath));
40
+ }
41
+ }
42
+ /** Reject module imports that resolve outside the source boundary. */
43
+ export function rejectOutsideSourcePlugin(boundary) {
13
44
  return {
14
45
  name: 'reject-artifact-outside-source',
15
46
  enforce: 'pre',
@@ -21,26 +52,9 @@ export function rejectOutsideSourcePlugin({ root, trustedExternalRoots, }) {
21
52
  if (!isAbsolute(filePath))
22
53
  return resolved;
23
54
  // Vite may return a physical path for a root reached through /tmp or
24
- // another symlink. Check physical paths on both sides of the boundary;
25
- // this also prevents an in-root symlink from admitting an outside file.
26
- const realFilePath = realpathSync(filePath);
27
- if (isPathWithin(rootResolved, realFilePath))
28
- return resolved;
29
- const importerPath = importer?.split('?')[0];
30
- const importerIsTrustedExternal = importerPath != null &&
31
- (isNodeModulesPath(importerPath) ||
32
- (isAbsolute(importerPath) && isTrustedExternalPath(realpathSync(importerPath), trustedRoots)));
33
- if (isNodeModulesPath(filePath) && (isBareImport(source) || importerIsTrustedExternal)) {
34
- return resolved;
35
- }
36
- if (isTrustedExternalPath(realFilePath, trustedRoots))
37
- return resolved;
38
- const from = importer
39
- ? ` (imported from ${relative(rootResolved, importer.split('?')[0])})`
40
- : '';
41
- throw new Error(`Artifact import "${filePath}" resolves outside artifact source directory ` +
42
- `"${rootResolved}"${from}. Keep relative build inputs inside the artifact ` +
43
- 'source directory or publish shared code as a dependency.');
55
+ // another symlink; the boundary compares physical paths on both sides.
56
+ boundary.assertBuildInput({ filePath, importer, bareSpecifier: isBareImport(source), reference: 'import' });
57
+ return resolved;
44
58
  },
45
59
  };
46
60
  }
@@ -62,6 +76,6 @@ function isTrustedExternalPath(filePath, trustedRoots) {
62
76
  export function isNodeModulesPath(filePath) {
63
77
  return filePath.replaceAll('\\', '/').includes('/node_modules/');
64
78
  }
65
- function isBareImport(source) {
79
+ export function isBareImport(source) {
66
80
  return !source.startsWith('.') && !isAbsolute(source);
67
81
  }
@@ -0,0 +1,4 @@
1
+ /** Shared upload exclusion policy. Content scanning and filesystem containment remain separate checks. */
2
+ export declare function isExcludedArtifactSourcePath({ path }: {
3
+ path: string;
4
+ }): boolean;
@@ -0,0 +1,16 @@
1
+ /** Leaf module (no imports) so manifest and source-include validation can share it without a cycle. */
2
+ const excludedSourceSegments = new Set([
3
+ '.git', 'node_modules', '.next', '.npmrc', '.netrc', '.aws', '.ssh', '.config',
4
+ 'dist', '.artifact-studio', '.git-credentials', '.pypirc', '.azure', '.gcloud', '.doppler',
5
+ '.turbo', 'coverage', '.docker', '.seq-next-output.json',
6
+ ]);
7
+ /** Shared upload exclusion policy. Content scanning and filesystem containment remain separate checks. */
8
+ export function isExcludedArtifactSourcePath({ path }) {
9
+ return path.split('/').some((segment) => {
10
+ const normalized = segment.toLowerCase();
11
+ return excludedSourceSegments.has(normalized)
12
+ || normalized.startsWith('.env') || normalized.startsWith('.yarnrc')
13
+ || /(?:^|[-_.])(?:credentials?|service[-_]?account)(?:[-_.].*)?\.json$/.test(normalized)
14
+ || /\.(pem|key|p12|pfx|jks)$/.test(normalized);
15
+ });
16
+ }
@@ -4,7 +4,7 @@ import { mkdtemp, realpath, rm, stat } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
6
6
  import { createScrubbedChildEnv } from './child-environment.js';
7
- import { materializeRepo, resolveCommitSha, resolveRepo } from './git-service-client.js';
7
+ import { checkRepositoryDeploymentIdentity, materializeRepo, resolveCommitSha, resolveRepo } from './git-service-client.js';
8
8
  import { gitServiceCloneUrl, runGitClone } from './git-clone.js';
9
9
  /** True for sources that must be fetched from a remote (git-service / git-url). */
10
10
  export function isRemoteSpec(spec) {
@@ -126,6 +126,7 @@ export async function resolveArtifactSource(spec, opts = {}) {
126
126
  const repo = await resolveRepo({ baseUrl: opts.baseUrl, token: opts.token, namespace: spec.namespace, name: spec.name });
127
127
  const ref = spec.ref ?? repo.defaultBranch;
128
128
  const sha = await resolveCommitSha({ baseUrl: opts.baseUrl, token: opts.token, repoId: repo.id, ref });
129
+ await checkRepositoryDeploymentIdentity({ baseUrl: opts.baseUrl, token: opts.token, repoId: repo.id, commit: sha });
129
130
  // CI/system path: the trusted M2M service account can't own a PAT (PAT
130
131
  // issuance is gated on a human Auth0 user row), so it keeps the JSON
131
132
  // materialize path it has always used, authenticated by its M2M token.
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "publishConfig": {
10
10
  "access": "public"
11
11
  },
12
- "version": "0.9.0",
12
+ "version": "0.9.2",
13
13
  "description": "SDK types and CLI library for building Artifact Studio apps. Driven via `seq-studio artifact <sub>` (@sequenceholdings/studio-cli), which runs the `./cli` runCli export.",
14
14
  "type": "module",
15
15
  "exports": {
@@ -33,6 +33,10 @@
33
33
  "types": "./dist/git-service-client.d.ts",
34
34
  "default": "./dist/git-service-client.js"
35
35
  },
36
+ "./repository-identity": {
37
+ "types": "./dist/repository-identity.d.ts",
38
+ "default": "./dist/repository-identity.js"
39
+ },
36
40
  "./git-clone": {
37
41
  "types": "./dist/git-clone.d.ts",
38
42
  "default": "./dist/git-clone.js"
@@ -168,8 +172,8 @@
168
172
  "server-only": "0.0.1",
169
173
  "parse5": "7.3.0",
170
174
  "@sequenceholdings/lattice-form-renderer": "0.2.0",
171
- "@sequenceholdings/storage": "0.1.1",
172
- "@sequenceholdings/orm": "2.6.2"
175
+ "@sequenceholdings/orm": "2.6.2",
176
+ "@sequenceholdings/storage": "0.1.2"
173
177
  },
174
178
  "devDependencies": {
175
179
  "@types/js-yaml": "^4.0.9",