@sequenceholdings/artifact-studio 0.9.1 → 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.
- package/dist/artifact-env-bindings.d.ts +1 -1
- package/dist/artifact-env-bindings.js +5 -4
- package/dist/build-input-guards.d.ts +8 -0
- package/dist/build-input-guards.js +178 -0
- package/dist/build.js +14 -5
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +30 -5
- package/dist/{env-binding-scan.d.ts → env-binding-manifest.d.ts} +1 -4
- package/dist/env-binding-manifest.js +56 -0
- package/dist/env-bindings.js +2 -34
- package/dist/git-service-client.d.ts +11 -0
- package/dist/git-service-client.js +10 -0
- package/dist/manifest-next.d.ts +6 -0
- package/dist/manifest-next.js +3 -0
- package/dist/manifest.d.ts +4 -0
- package/dist/manifest.js +1 -0
- package/dist/next-source-includes.d.ts +50 -0
- package/dist/next-source-includes.js +68 -0
- package/dist/prepare-build.d.ts +10 -2
- package/dist/prepare-build.js +33 -3
- package/dist/repository-identity.d.ts +31 -0
- package/dist/repository-identity.js +41 -0
- package/dist/runtime-contracts.d.ts +1 -4
- package/dist/runtime-contracts.js +2 -15
- package/dist/source-boundary.d.ts +27 -6
- package/dist/source-boundary.js +44 -30
- package/dist/source-exclusions.d.ts +4 -0
- package/dist/source-exclusions.js +16 -0
- package/dist/source-resolver.js +2 -1
- package/package.json +7 -3
- package/dist/env-binding-scan.js +0 -179
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Validate explicit artifact manifest bindings against the selected environment. */
|
|
2
2
|
export declare function assertArtifactEnvironmentBindings({ directory, environment, token, }: {
|
|
3
3
|
directory: string;
|
|
4
4
|
environment: {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { getJsonOr404 } from './api.js';
|
|
2
4
|
import { knownEnvUrls } from './config.js';
|
|
3
5
|
import { assertNoCrossEnvironmentBindings, canonicalDeploymentOrigin } from './env-bindings.js';
|
|
4
|
-
|
|
5
|
-
/** Fail an artifact deploy whose source still points at another environment. */
|
|
6
|
+
/** Validate explicit artifact manifest bindings against the selected environment. */
|
|
6
7
|
export async function assertArtifactEnvironmentBindings({ directory, environment, token, }) {
|
|
7
|
-
const
|
|
8
|
+
const manifest = await readFile(join(directory, 'artifact.bundle.yml'), 'utf8');
|
|
8
9
|
const deployOrigin = canonicalDeploymentOrigin(environment.url);
|
|
9
10
|
if (!deployOrigin)
|
|
10
11
|
throw new Error(`Invalid deployment URL '${environment.url}'.`);
|
|
@@ -17,7 +18,7 @@ export async function assertArtifactEnvironmentBindings({ directory, environment
|
|
|
17
18
|
deployOrigin,
|
|
18
19
|
origins,
|
|
19
20
|
workspaces: [],
|
|
20
|
-
files:
|
|
21
|
+
files: [{ path: 'artifact.bundle.yml', content: manifest }],
|
|
21
22
|
workspaceExists: async (workspaceId) => {
|
|
22
23
|
const workspace = await getJsonOr404({
|
|
23
24
|
baseUrl: environment.url,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Plugin } from 'vite';
|
|
2
|
+
import { type SourceBoundary } from './source-boundary.js';
|
|
3
|
+
export declare function guardStylesheetInputsPlugin(boundary: SourceBoundary): Plugin;
|
|
4
|
+
/**
|
|
5
|
+
* Reject `new URL('<path>', import.meta.url)` assets outside the boundary. A
|
|
6
|
+
* template literal is a glob over its static directory, which must stay inside.
|
|
7
|
+
*/
|
|
8
|
+
export declare function guardImportMetaUrlPlugin(boundary: SourceBoundary): Plugin;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { createIdResolver } from 'vite';
|
|
5
|
+
import { isBareImport } from './source-boundary.js';
|
|
6
|
+
/**
|
|
7
|
+
* Vite and Tailwind resolve some build inputs with internal resolvers that
|
|
8
|
+
* never reach a plugin's `resolveId`: stylesheet `@import`, CSS `url()` and
|
|
9
|
+
* `image-set()`, and `new URL('<literal>', import.meta.url)`. Tailwind also
|
|
10
|
+
* runs JavaScript named by `@plugin` and `@config`. The build runs on the
|
|
11
|
+
* caller's machine (a laptop, CI, or an agent sandbox holding the caller's
|
|
12
|
+
* token), so these guards check every such reference against the source
|
|
13
|
+
* boundary before Vite or Tailwind acts on it.
|
|
14
|
+
*/
|
|
15
|
+
/** The stylesheet requests Vite and Tailwind process (Vite's own CSS language test). */
|
|
16
|
+
const STYLESHEET_REQUEST_RE = /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)|[?&]lang\.css\b/;
|
|
17
|
+
/** Tailwind at-rules that load and run JavaScript at build time. */
|
|
18
|
+
const BUILD_CODE_AT_RULE_RE = /@(?:plugin|config)(?![\w-])/i;
|
|
19
|
+
/** A CSS string: either quote, with the other quote and escapes allowed inside. */
|
|
20
|
+
const CSS_STRING = String.raw `"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'`;
|
|
21
|
+
/** `@import` / `@reference` targets: a string, or a string or bare token in `url()`. */
|
|
22
|
+
const STYLESHEET_IMPORT_RE = new RegExp(String.raw `@(?:import|reference)\s*(?:url\(\s*)?(?:${CSS_STRING}|([^'"\s;)]+))`, 'gi');
|
|
23
|
+
/** Tailwind `@source` paths (`@source inline(...)` names classes, not files). */
|
|
24
|
+
const SOURCE_DIRECTIVE_RE = new RegExp(String.raw `@source\s+(?:not\s+)?(?:${CSS_STRING})`, 'gi');
|
|
25
|
+
/** Mirrors Vite's `cssUrlRE`, so every `url()` Vite rewrites is checked. */
|
|
26
|
+
const CSS_URL_RE = /(?<=^|[^\w\-\u0080-])url\((\s*('[^']+'|"[^"]+")\s*|(?:\\.|[^'")\\])+)\)/g;
|
|
27
|
+
const IMAGE_SET_RE = /image-set\(([^)]*(?:\([^)]*\)[^)]*)*)\)/gi;
|
|
28
|
+
const QUOTED_RE = new RegExp(CSS_STRING, 'g');
|
|
29
|
+
/** Mirrors Vite's `assetImportMetaUrlRE`. */
|
|
30
|
+
const IMPORT_META_URL_RE = /\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)/g;
|
|
31
|
+
const NON_FILE_URL_RE = /^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i;
|
|
32
|
+
function pathOf(id) {
|
|
33
|
+
if (id.startsWith('\0'))
|
|
34
|
+
return null;
|
|
35
|
+
const filePath = id.split('?')[0];
|
|
36
|
+
return isAbsolute(filePath) ? filePath : null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Where Vite would read a referenced asset from: relative to the referencing
|
|
40
|
+
* file, root-relative then absolute for a leading `/`. Null for a URL that is
|
|
41
|
+
* not a local file or that does not exist (Vite leaves those unresolved).
|
|
42
|
+
*/
|
|
43
|
+
function referencedFile({ reference, importer, root }) {
|
|
44
|
+
let target = reference.trim().replace(/^['"]|['"]$/g, '').split(/[?#]/)[0];
|
|
45
|
+
try {
|
|
46
|
+
target = decodeURI(target);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Vite keeps an undecodable URL as written.
|
|
50
|
+
}
|
|
51
|
+
if (!target || NON_FILE_URL_RE.test(target))
|
|
52
|
+
return null;
|
|
53
|
+
const candidates = target.startsWith('/') ? [join(root, target), target] : [resolve(dirname(importer), target)];
|
|
54
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Reject Tailwind JavaScript loading and out-of-boundary references in every
|
|
58
|
+
* stylesheet the build processes, following `@import` into nested stylesheets
|
|
59
|
+
* because Tailwind and Vite read those without another plugin pass. Must run
|
|
60
|
+
* before Tailwind: list it ahead of `@tailwindcss/vite`.
|
|
61
|
+
*/
|
|
62
|
+
/** The first capture group a match filled: the string body or the bare token. */
|
|
63
|
+
function captured(match) {
|
|
64
|
+
return match.slice(1).find((group) => group !== undefined);
|
|
65
|
+
}
|
|
66
|
+
export function guardStylesheetInputsPlugin(boundary) {
|
|
67
|
+
// Files already scanned or being scanned. Recording a file before awaiting
|
|
68
|
+
// its imports makes an import cycle end instead of awaiting an ancestor.
|
|
69
|
+
const started = new Set();
|
|
70
|
+
// Resolve package stylesheets as Vite's CSS @import does (the `style`
|
|
71
|
+
// condition): a CSS-only package such as tw-animate-css has no JS entry.
|
|
72
|
+
let resolveStylesheet;
|
|
73
|
+
async function checkStylesheet({ file, code, resolveStylesheetImport }) {
|
|
74
|
+
if (!boundary.isTrusted(file) && BUILD_CODE_AT_RULE_RE.test(code)) {
|
|
75
|
+
throw new Error(`Artifact stylesheet "${file}" uses Tailwind @plugin or @config, which runs JavaScript on the build machine. ` +
|
|
76
|
+
'Configure Tailwind in CSS (@theme, @utility, @custom-variant) instead.');
|
|
77
|
+
}
|
|
78
|
+
const assertPath = (reference, kind) => {
|
|
79
|
+
const filePath = referencedFile({ reference, importer: file, root: boundary.root });
|
|
80
|
+
if (filePath)
|
|
81
|
+
boundary.assertBuildInput({ filePath, importer: file, bareSpecifier: false, reference: kind });
|
|
82
|
+
return filePath;
|
|
83
|
+
};
|
|
84
|
+
const nested = [];
|
|
85
|
+
for (const match of code.matchAll(STYLESHEET_IMPORT_RE)) {
|
|
86
|
+
const specifier = captured(match);
|
|
87
|
+
if (!specifier || NON_FILE_URL_RE.test(specifier))
|
|
88
|
+
continue;
|
|
89
|
+
// Check both the literal path and what the CSS resolver finds (which adds
|
|
90
|
+
// `.css` to an extensionless path and follows a package's `style` entry):
|
|
91
|
+
// Tailwind and Vite load whichever exists.
|
|
92
|
+
const literal = assertPath(specifier, 'stylesheet import');
|
|
93
|
+
const resolved = await resolveStylesheetImport(specifier, file);
|
|
94
|
+
if (resolved) {
|
|
95
|
+
boundary.assertBuildInput({ filePath: resolved, importer: file, bareSpecifier: isBareImport(specifier), reference: 'stylesheet import' });
|
|
96
|
+
}
|
|
97
|
+
// Tailwind and Vite parse any imported file as CSS, whatever its extension.
|
|
98
|
+
for (const filePath of new Set([literal, resolved]))
|
|
99
|
+
if (filePath)
|
|
100
|
+
nested.push(filePath);
|
|
101
|
+
}
|
|
102
|
+
for (const match of code.matchAll(SOURCE_DIRECTIVE_RE)) {
|
|
103
|
+
const path = captured(match);
|
|
104
|
+
if (path)
|
|
105
|
+
boundary.assertBuildInput({ filePath: resolve(dirname(file), path), importer: file, bareSpecifier: false, reference: '@source path' });
|
|
106
|
+
}
|
|
107
|
+
for (const [, reference] of code.matchAll(CSS_URL_RE)) {
|
|
108
|
+
if (reference)
|
|
109
|
+
assertPath(reference, 'stylesheet url()');
|
|
110
|
+
}
|
|
111
|
+
for (const [, body] of code.matchAll(IMAGE_SET_RE)) {
|
|
112
|
+
for (const quoted of (body ?? '').matchAll(QUOTED_RE)) {
|
|
113
|
+
const reference = captured(quoted);
|
|
114
|
+
if (reference)
|
|
115
|
+
assertPath(reference, 'stylesheet image-set()');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
await Promise.all(nested.map((nestedFile) => checkFile(nestedFile, resolveStylesheetImport)));
|
|
119
|
+
}
|
|
120
|
+
async function checkFile(file, resolveStylesheetImport) {
|
|
121
|
+
// A file another traversal already started is checked there, and a failure
|
|
122
|
+
// there fails the build.
|
|
123
|
+
if (started.has(file))
|
|
124
|
+
return;
|
|
125
|
+
started.add(file);
|
|
126
|
+
await checkStylesheet({ file, code: await readFile(file, 'utf8'), resolveStylesheetImport });
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
name: 'guard-artifact-stylesheet-inputs',
|
|
130
|
+
enforce: 'pre',
|
|
131
|
+
configResolved(config) {
|
|
132
|
+
resolveStylesheet = createIdResolver(config, {
|
|
133
|
+
extensions: ['.css'], mainFields: ['style'], conditions: ['style', 'production'], tryIndex: false, preferRelative: true,
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
async transform(code, id) {
|
|
137
|
+
const file = pathOf(id);
|
|
138
|
+
if (!file || !STYLESHEET_REQUEST_RE.test(id) || /[?&]raw\b/.test(id))
|
|
139
|
+
return null;
|
|
140
|
+
const environment = this.environment;
|
|
141
|
+
const resolveStylesheetImport = async (specifier, importer) => {
|
|
142
|
+
// An import the CSS resolver cannot find is checked by its literal path;
|
|
143
|
+
// anything else unresolvable fails in Vite or Tailwind as well.
|
|
144
|
+
const resolved = await resolveStylesheet?.(environment, specifier, importer).catch(() => undefined);
|
|
145
|
+
return resolved ? pathOf(resolved) : null;
|
|
146
|
+
};
|
|
147
|
+
await checkStylesheet({ file, code, resolveStylesheetImport });
|
|
148
|
+
return null;
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Reject `new URL('<path>', import.meta.url)` assets outside the boundary. A
|
|
154
|
+
* template literal is a glob over its static directory, which must stay inside.
|
|
155
|
+
*/
|
|
156
|
+
export function guardImportMetaUrlPlugin(boundary) {
|
|
157
|
+
return {
|
|
158
|
+
name: 'guard-artifact-import-meta-url',
|
|
159
|
+
enforce: 'pre',
|
|
160
|
+
transform(code, id) {
|
|
161
|
+
const file = pathOf(id);
|
|
162
|
+
if (!file || STYLESHEET_REQUEST_RE.test(id) || !code.includes('import.meta.url'))
|
|
163
|
+
return null;
|
|
164
|
+
for (const [, literal] of code.matchAll(IMPORT_META_URL_RE)) {
|
|
165
|
+
const body = literal.slice(1, -1);
|
|
166
|
+
const isTemplate = literal.startsWith('`') && body.includes('${');
|
|
167
|
+
const reference = isTemplate ? body.slice(0, body.lastIndexOf('/', body.indexOf('${')) + 1) || '.' : body;
|
|
168
|
+
if (NON_FILE_URL_RE.test(reference))
|
|
169
|
+
continue;
|
|
170
|
+
const target = reference.startsWith('/')
|
|
171
|
+
? [join(boundary.root, reference), reference].find((candidate) => existsSync(candidate)) ?? join(boundary.root, reference)
|
|
172
|
+
: resolve(dirname(file), reference);
|
|
173
|
+
boundary.assertBuildInput({ filePath: target, importer: file, bareSpecifier: false, reference: 'new URL() asset' });
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
package/dist/build.js
CHANGED
|
@@ -15,7 +15,8 @@ import { isNextArtifactManifest } from './manifest.js';
|
|
|
15
15
|
import { normalizeArtifactPath } from './paths.js';
|
|
16
16
|
import { resolveArtifactStudioPeerVersions, } from './peer-versions.js';
|
|
17
17
|
import { ARTIFACT_RUNTIME_CHUNK_DIR, assertSafeRuntimeChunkFileName } from './runtime-chunks.js';
|
|
18
|
-
import {
|
|
18
|
+
import { guardImportMetaUrlPlugin, guardStylesheetInputsPlugin } from './build-input-guards.js';
|
|
19
|
+
import { createSourceBoundary, rejectOutsideSourcePlugin } from './source-boundary.js';
|
|
19
20
|
import { collectCandidateTokenCompatibilityDiagnostics, collectDeprecatedTokenImportDiagnostics, } from './token-contract.js';
|
|
20
21
|
/** Keep in sync with `atlas/src/server/services/artifact-studio/limits.ts`. */
|
|
21
22
|
const MAX_DEPLOY_SOURCEMAP_BYTES = 10 * 1024 * 1024;
|
|
@@ -126,16 +127,24 @@ export async function buildArtifactStudioProject(rootDir, { candidateTokenCompat
|
|
|
126
127
|
// Give Vite and the source boundary the same physical root.
|
|
127
128
|
const root = await realpath(source.root);
|
|
128
129
|
const transformedCssModules = new Map();
|
|
130
|
+
const boundary = createSourceBoundary({
|
|
131
|
+
root,
|
|
132
|
+
trustedExternalRoots: [...Object.values(peerResolveAlias).map((target) => dirname(target)), dirname(artifactSdkEntry)],
|
|
133
|
+
});
|
|
129
134
|
const result = await viteBuild({
|
|
130
135
|
root,
|
|
131
136
|
configFile: false,
|
|
132
137
|
logLevel: 'silent',
|
|
138
|
+
// An inline PostCSS config stops Vite from loading postcss.config.* (or
|
|
139
|
+
// .postcssrc) from the source tree, which would run repository code on the
|
|
140
|
+
// build machine. Tailwind runs as the Vite plugin below and needs none.
|
|
141
|
+
css: { postcss: { plugins: [] } },
|
|
133
142
|
plugins: [
|
|
134
|
-
rejectOutsideSourcePlugin(
|
|
135
|
-
root,
|
|
136
|
-
trustedExternalRoots: [...Object.values(peerResolveAlias).map((target) => dirname(target)), dirname(artifactSdkEntry)],
|
|
137
|
-
}),
|
|
143
|
+
rejectOutsideSourcePlugin(boundary),
|
|
138
144
|
rejectIgnoredSourcePlugin(root, source.ignorePatterns),
|
|
145
|
+
// Before Tailwind, which reads @import / @plugin / @config itself.
|
|
146
|
+
guardStylesheetInputsPlugin(boundary),
|
|
147
|
+
guardImportMetaUrlPlugin(boundary),
|
|
139
148
|
artifactSdkVirtualModule(),
|
|
140
149
|
atlasUiCssResolverPlugin({
|
|
141
150
|
atlasUiDir: selectedAtlasUiDir,
|
package/dist/cli.d.ts
CHANGED
|
@@ -39,5 +39,6 @@ export interface ProvidedToken {
|
|
|
39
39
|
authMode: SourceAuthMode;
|
|
40
40
|
token: string;
|
|
41
41
|
}
|
|
42
|
+
export declare function setPackageInstallUnavailable(guidance: string | null): void;
|
|
42
43
|
export declare function setTokenProvider(provider: ((options: TokenProviderOptions) => Promise<ProvidedToken | null>) | null): void;
|
|
43
44
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -64,6 +64,7 @@ const USAGE = `usage:
|
|
|
64
64
|
select the target environment. Next.js build/plan/deploy accept --source-env
|
|
65
65
|
to read a Git Service repository from a separately authenticated environment.
|
|
66
66
|
Log in to both environments; source read and target deploy permissions apply.
|
|
67
|
+
Source identity must be adopted and belong to the destination tenant.
|
|
67
68
|
The target must support verified cross-environment builds. Source credentials
|
|
68
69
|
are used only to fetch source, never passed to the build or stored with it.
|
|
69
70
|
|
|
@@ -397,6 +398,19 @@ async function showCommand(args) {
|
|
|
397
398
|
}
|
|
398
399
|
return 0;
|
|
399
400
|
}
|
|
401
|
+
function observedEvidenceLine(evidence) {
|
|
402
|
+
if ('atlasUi' in evidence) {
|
|
403
|
+
return `${evidence.channel} / ${evidence.integrityReceipt?.status ?? 'no receipt'} / ${evidence.toolchain.selection} toolchain ` +
|
|
404
|
+
`(atlas-ui ${evidence.atlasUi.contentHash.slice(0, 12)})`;
|
|
405
|
+
}
|
|
406
|
+
const toolchain = evidence.toolchain;
|
|
407
|
+
const parts = [
|
|
408
|
+
`Next.js ${toolchain?.next ?? '?'} on Node ${toolchain?.node ?? '?'}`,
|
|
409
|
+
evidence.builderImage ? `builder ${evidence.builderImage.replace(/^sha256:/, '').slice(0, 12)}` : undefined,
|
|
410
|
+
evidence.lockfileSha256 ? `lockfile ${evidence.lockfileSha256.slice(0, 12)}` : undefined,
|
|
411
|
+
];
|
|
412
|
+
return parts.filter(Boolean).join(' / ');
|
|
413
|
+
}
|
|
400
414
|
function logActiveDeploymentDetails(deployment) {
|
|
401
415
|
console.log(` version: ${deployment.version}`);
|
|
402
416
|
console.log(` sourceHash: ${deployment.sourceHash}`);
|
|
@@ -408,10 +422,8 @@ function logActiveDeploymentDetails(deployment) {
|
|
|
408
422
|
if (deployment.cliVersion || deployment.atlasUiVersion) {
|
|
409
423
|
console.log(` peers: artifact-studio@${deployment.cliVersion ?? '?'} atlas-ui@${deployment.atlasUiVersion ?? '?'}`);
|
|
410
424
|
}
|
|
411
|
-
if (deployment.observedBuildEvidence)
|
|
412
|
-
console.log(` observed: ${deployment.observedBuildEvidence
|
|
413
|
-
`(atlas-ui ${deployment.observedBuildEvidence.atlasUi.contentHash.slice(0, 12)})`);
|
|
414
|
-
}
|
|
425
|
+
if (deployment.observedBuildEvidence)
|
|
426
|
+
console.log(` observed: ${observedEvidenceLine(deployment.observedBuildEvidence)}`);
|
|
415
427
|
if (deployment.deployMessage)
|
|
416
428
|
console.log(` message: ${deployment.deployMessage}`);
|
|
417
429
|
if (deployment.createdAt)
|
|
@@ -435,7 +447,10 @@ async function validateCommand(args) {
|
|
|
435
447
|
return 0;
|
|
436
448
|
}
|
|
437
449
|
async function buildPreparedArtifactSource(source) {
|
|
438
|
-
await prepareArtifactBuildRoot(source.dir, {
|
|
450
|
+
await prepareArtifactBuildRoot(source.dir, {
|
|
451
|
+
remote: source.remote,
|
|
452
|
+
...(packageInstallUnavailable ? { installUnavailable: packageInstallUnavailable } : {}),
|
|
453
|
+
});
|
|
439
454
|
return buildArtifactStudioProjectIsolated(source.dir);
|
|
440
455
|
}
|
|
441
456
|
function reportBuildDiagnostics(result) {
|
|
@@ -1066,6 +1081,16 @@ function assertSourceEnvironmentSupportsManifest(args) {
|
|
|
1066
1081
|
throw new Error('--source-env is supported for Next.js repository builds only.');
|
|
1067
1082
|
}
|
|
1068
1083
|
let tokenProvider = null;
|
|
1084
|
+
/**
|
|
1085
|
+
* Embedder guidance for a host that cannot reach a package registry, such as an
|
|
1086
|
+
* agent sandbox. When set, a remote build whose package.json declares
|
|
1087
|
+
* production dependencies fails with it instead of attempting an install.
|
|
1088
|
+
* Next.js applications build in Atlas and are unaffected.
|
|
1089
|
+
*/
|
|
1090
|
+
let packageInstallUnavailable = null;
|
|
1091
|
+
export function setPackageInstallUnavailable(guidance) {
|
|
1092
|
+
packageInstallUnavailable = guidance;
|
|
1093
|
+
}
|
|
1069
1094
|
export function setTokenProvider(provider) {
|
|
1070
1095
|
tokenProvider = provider;
|
|
1071
1096
|
}
|
|
@@ -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
|
+
}
|
package/dist/env-bindings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { basename, collectWorkspaceSlotIds,
|
|
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
|
|
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
|
package/dist/manifest-next.d.ts
CHANGED
|
@@ -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<{
|
package/dist/manifest-next.js
CHANGED
|
@@ -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');
|
package/dist/manifest.d.ts
CHANGED
|
@@ -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,
|
|
@@ -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;
|