@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.
- package/README.md +219 -0
- package/VERSION-PIN.md +15 -0
- package/dist/api.d.ts +5 -0
- package/dist/api.js +7 -0
- 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-content-readers.d.ts +8 -0
- package/dist/cli-content-readers.js +28 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +38 -6
- 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/bridge.d.ts +15 -1
- package/dist/next/bridge.js +18 -1
- 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/server/api-client.d.ts +1 -0
- package/dist/server/api-client.js +13 -7
- package/dist/server/group-directory.d.ts +90 -0
- package/dist/server/group-directory.js +17 -0
- package/dist/server/groups.d.ts +2 -1
- package/dist/server/groups.js +2 -0
- package/dist/server/human-completion.d.ts +44 -0
- package/dist/server/human-completion.js +38 -0
- package/dist/server/processes.d.ts +19 -0
- package/dist/server/processes.js +11 -0
- package/dist/server/task-assignments.d.ts +81 -0
- package/dist/server/task-assignments.js +26 -0
- package/dist/server.d.ts +4 -0
- 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/dist/templates/react-vite/package.json +1 -1
- package/package.json +6 -2
- package/templates/react-vite/package.json +1 -1
- 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
|
|
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, {
|
|
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
|
+
}
|
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,
|
package/dist/next/bridge.d.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/dist/next/bridge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/prepare-build.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
/**
|
package/dist/prepare-build.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 (!
|
|
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;
|