@sequenceholdings/artifact-studio 0.4.0 → 0.6.1
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 +35 -1
- package/VERSION-PIN.md +29 -0
- package/dist/active-deploy-policy.d.ts +9 -3
- package/dist/active-deploy-policy.js +20 -8
- package/dist/artifact-env-bindings.d.ts +9 -0
- package/dist/artifact-env-bindings.js +32 -0
- package/dist/build.js +6 -0
- package/dist/cli.js +62 -13
- package/dist/deployment-validation.d.ts +2 -0
- package/dist/deployment-validation.js +2 -0
- package/dist/dev-link.js +28 -2
- package/dist/env-binding-scan.d.ts +10 -0
- package/dist/env-binding-scan.js +179 -0
- package/dist/env-bindings.d.ts +52 -0
- package/dist/env-bindings.js +180 -0
- package/dist/gateway/control.d.ts +33 -0
- package/dist/gateway/control.js +29 -0
- package/dist/gateway/hosted-proxy.d.ts +5 -0
- package/dist/gateway/hosted-proxy.js +4 -1
- package/dist/gateway/lattice-invoke.d.ts +48 -0
- package/dist/gateway/lattice-invoke.js +245 -0
- package/dist/gateway/lattice-upstream.d.ts +38 -0
- package/dist/gateway/lattice-upstream.js +136 -0
- package/dist/gateway/server.js +2 -3
- package/dist/gateway/session.d.ts +3 -1
- package/dist/gateway/session.js +10 -3
- package/dist/manifest-next.d.ts +55 -0
- package/dist/manifest-next.js +35 -2
- package/dist/manifest-static.d.ts +35 -0
- package/dist/manifest-static.js +12 -0
- package/dist/manifest.d.ts +48 -0
- package/dist/next-build.d.ts +2 -1
- package/dist/next-build.js +12 -7
- package/dist/offline/cache.d.ts +4 -0
- package/dist/offline/cache.js +7 -0
- package/dist/offline/connector.d.ts +28 -0
- package/dist/offline/connector.js +122 -0
- package/dist/offline/crypto.d.ts +5 -0
- package/dist/offline/crypto.js +24 -0
- package/dist/offline/index.d.ts +8 -0
- package/dist/offline/index.js +7 -0
- package/dist/offline/indexed-db.d.ts +10 -0
- package/dist/offline/indexed-db.js +104 -0
- package/dist/offline/management-types.d.ts +25 -0
- package/dist/offline/management-types.js +1 -0
- package/dist/offline/outbox.d.ts +7 -0
- package/dist/offline/outbox.js +46 -0
- package/dist/offline/replay.d.ts +25 -0
- package/dist/offline/replay.js +192 -0
- package/dist/offline/secure-store.d.ts +6 -0
- package/dist/offline/secure-store.js +51 -0
- package/dist/offline/types.d.ts +162 -0
- package/dist/offline/types.js +1 -0
- package/dist/orm-json-scalars.d.ts +21 -0
- package/dist/orm-json-scalars.js +21 -0
- package/dist/runtime-contracts.d.ts +7 -7
- package/dist/runtime-contracts.js +1 -1
- package/dist/sdk-orm.d.ts +7 -1
- package/dist/sdk-orm.js +12 -5
- package/dist/sdk.d.ts +134 -1
- package/dist/server/api-client.d.ts +26 -0
- package/dist/server/api-client.js +38 -0
- package/dist/server/broker-contract.d.ts +191 -5
- package/dist/server/broker-contract.js +142 -1
- package/dist/server/client.d.ts +82 -47
- package/dist/server/client.js +138 -59
- package/dist/server/connection-retry.d.ts +12 -0
- package/dist/server/connection-retry.js +40 -0
- package/dist/server/errors.d.ts +61 -0
- package/dist/server/errors.js +89 -0
- package/dist/server/external-context.d.ts +21 -0
- package/dist/server/external-context.js +19 -0
- package/dist/server/internal/signed-token.d.ts +15 -0
- package/dist/server/internal/signed-token.js +67 -0
- package/dist/server/internal/v1-context.d.ts +18 -0
- package/dist/server/internal/v1-context.js +26 -0
- package/dist/server/internal/verify-any.d.ts +22 -0
- package/dist/server/internal/verify-any.js +53 -0
- package/dist/server/logger.d.ts +1 -1
- package/dist/server/managed-server-path.d.ts +6 -0
- package/dist/server/managed-server-path.js +14 -0
- package/dist/server/principal.d.ts +48 -0
- package/dist/server/principal.js +39 -0
- package/dist/server/processes.d.ts +290 -0
- package/dist/server/processes.js +56 -0
- package/dist/server/request-context.d.ts +121 -3
- package/dist/server/request-context.js +111 -33
- package/dist/server/testing.d.ts +40 -0
- package/dist/server/testing.js +86 -0
- package/dist/server.d.ts +13 -4
- package/dist/server.js +6 -2
- package/dist/source-resolver.d.ts +9 -2
- package/dist/source-resolver.js +55 -8
- package/dist/templates/nextjs/AGENTS.md +10 -0
- package/dist/templates/nextjs/CLAUDE.md +1 -0
- package/dist/templates/nextjs/README.md +88 -0
- package/dist/templates/nextjs/package.json +3 -1
- package/dist/templates/nextjs/tests/server.test.mjs +25 -0
- package/dist/templates/react-vite/AGENTS.md +8 -88
- package/dist/templates/react-vite/CLAUDE.md +0 -6
- package/dist/templates/react-vite/package.json +1 -1
- package/dist/templates/react-vite/src/App.tsx +37 -14
- package/dist/templates/react-vite/src/styles.css +10 -0
- package/package.json +15 -3
- package/templates/nextjs/AGENTS.md +10 -0
- package/templates/nextjs/CLAUDE.md +1 -0
- package/templates/nextjs/README.md +88 -0
- package/templates/nextjs/package.json +3 -1
- package/templates/nextjs/tests/server.test.mjs +25 -0
- package/templates/react-vite/AGENTS.md +8 -88
- package/templates/react-vite/CLAUDE.md +0 -6
- package/templates/react-vite/package.json +1 -1
- package/templates/react-vite/src/App.tsx +37 -14
- package/templates/react-vite/src/styles.css +10 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type KeyObject } from 'node:crypto';
|
|
2
|
+
import { type RequestContextV2External } from './request-context.js';
|
|
3
|
+
import { type ArtifactRuntimeConfig } from './runtime-config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Test helpers for applications that accept external callers: sign an external (v2) context
|
|
6
|
+
* with a test key, and run code against an explicit runtime configuration. For unit tests and
|
|
7
|
+
* local `next dev` only. A context signed here verifies only against the matching public key,
|
|
8
|
+
* which a hosted runtime never pins.
|
|
9
|
+
*/
|
|
10
|
+
type ExternalPrincipalClaims = RequestContextV2External['principal'];
|
|
11
|
+
export interface SignTestExternalContextInput {
|
|
12
|
+
privateKey: KeyObject | string;
|
|
13
|
+
/** Use an `external-` kid; verifiers refuse any other kid on an external context. */
|
|
14
|
+
kid: string;
|
|
15
|
+
projectId: string;
|
|
16
|
+
environment: string;
|
|
17
|
+
method: string;
|
|
18
|
+
path: string;
|
|
19
|
+
/** Overrides on a verified-borrower default principal. */
|
|
20
|
+
principal?: Partial<Omit<ExternalPrincipalClaims, 'kind'>>;
|
|
21
|
+
serviceAccountId: string;
|
|
22
|
+
/** Defaults to `published`. Verifiers refuse `development` until development leases admit it. */
|
|
23
|
+
mode?: RequestContextV2External['mode'];
|
|
24
|
+
deploymentId?: string | null;
|
|
25
|
+
devSessionId?: string | null;
|
|
26
|
+
/** Seconds since the epoch; defaults to now. */
|
|
27
|
+
now?: number;
|
|
28
|
+
/** Defaults to and may not exceed 60. */
|
|
29
|
+
ttlSeconds?: number;
|
|
30
|
+
/** Overrides applied after the defaults, unvalidated, for negative tests. */
|
|
31
|
+
claims?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
export declare function signTestExternalContext({ privateKey, kid, projectId, environment, method, path, principal, serviceAccountId, mode, deploymentId, devSessionId, now, ttlSeconds, claims, }: SignTestExternalContextInput): string;
|
|
34
|
+
/**
|
|
35
|
+
* Runs `fn` with the hosted runtime configuration set to `config` (and no runtime file), then
|
|
36
|
+
* restores the previous environment. The override is process-wide: do not run overlapping calls.
|
|
37
|
+
* Refuses to run in a production build.
|
|
38
|
+
*/
|
|
39
|
+
export declare function withTestRuntimeConfig<T>(config: ArtifactRuntimeConfig, fn: () => Promise<T> | T): Promise<T>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, randomUUID, sign } from 'node:crypto';
|
|
2
|
+
import { ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS, requestContextV2ExternalSchema } from './request-context.js';
|
|
3
|
+
import { artifactRuntimeConfigSchema } from './runtime-config.js';
|
|
4
|
+
const defaultUserAgent = 'Mozilla/5.0 (test)';
|
|
5
|
+
export function signTestExternalContext({ privateKey, kid, projectId, environment, method, path, principal = {}, serviceAccountId, mode = 'published', deploymentId, devSessionId, now = Math.floor(Date.now() / 1000), ttlSeconds = ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS, claims = {}, }) {
|
|
6
|
+
const userAgent = principal.client?.userAgent ?? defaultUserAgent;
|
|
7
|
+
const resolvedPrincipal = {
|
|
8
|
+
kind: 'external',
|
|
9
|
+
ingress: 'borrower',
|
|
10
|
+
provider: 'test-provider',
|
|
11
|
+
issuer: 'https://idp.example.test/',
|
|
12
|
+
subject: 'test|borrower-1',
|
|
13
|
+
clientId: 'test-client',
|
|
14
|
+
email: 'borrower@example.test',
|
|
15
|
+
emailVerified: true,
|
|
16
|
+
authTime: now - 30,
|
|
17
|
+
amr: ['pwd'],
|
|
18
|
+
tokenIssuedAt: now - 30,
|
|
19
|
+
...principal,
|
|
20
|
+
pass: { id: randomUUID(), kid: 'pass-1', issuedAt: now - 10, exchangeIp: '203.0.113.10', ...principal.pass },
|
|
21
|
+
client: {
|
|
22
|
+
ip: '203.0.113.10',
|
|
23
|
+
userAgentSha256: createHash('sha256').update(userAgent).digest('hex'),
|
|
24
|
+
origin: 'https://borrower.example.test',
|
|
25
|
+
...principal.client,
|
|
26
|
+
userAgent,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const published = mode === 'published';
|
|
30
|
+
const payload = {
|
|
31
|
+
v: 2,
|
|
32
|
+
iss: 'atlas-artifact-gateway',
|
|
33
|
+
aud: `artifact:${projectId}`,
|
|
34
|
+
sub: `ext|${resolvedPrincipal.provider}|${resolvedPrincipal.subject}`,
|
|
35
|
+
projectId,
|
|
36
|
+
environment,
|
|
37
|
+
mode,
|
|
38
|
+
deploymentId: deploymentId !== undefined ? deploymentId : published ? 'test-deployment' : null,
|
|
39
|
+
devSessionId: devSessionId !== undefined ? devSessionId : published ? null : 'test-dev-session',
|
|
40
|
+
requestId: randomUUID(),
|
|
41
|
+
jti: randomUUID(),
|
|
42
|
+
method: method.toUpperCase(),
|
|
43
|
+
path,
|
|
44
|
+
iat: now,
|
|
45
|
+
exp: now + ttlSeconds,
|
|
46
|
+
serviceAccountId,
|
|
47
|
+
principal: resolvedPrincipal,
|
|
48
|
+
crit: [],
|
|
49
|
+
ext: {},
|
|
50
|
+
...claims,
|
|
51
|
+
};
|
|
52
|
+
// Catch a mistaken default early; `claims` overrides are for deliberately invalid contexts.
|
|
53
|
+
if (Object.keys(claims).length === 0)
|
|
54
|
+
requestContextV2ExternalSchema.parse(payload);
|
|
55
|
+
const key = typeof privateKey === 'string' ? createPrivateKey(privateKey) : privateKey;
|
|
56
|
+
const unsigned = [{ alg: 'EdDSA', typ: 'JWT', kid }, payload]
|
|
57
|
+
.map(part => Buffer.from(JSON.stringify(part)).toString('base64url')).join('.');
|
|
58
|
+
return `${unsigned}.${sign(null, Buffer.from(unsigned), key).toString('base64url')}`;
|
|
59
|
+
}
|
|
60
|
+
const runtimeEnvironmentKeys = ['SEQUENCE_ARTIFACT_RUNTIME_FILE', 'SEQUENCE_ARTIFACT_RUNTIME_CONFIG', 'SEQUENCE_ARTIFACT_RUNTIME_TOKEN'];
|
|
61
|
+
/**
|
|
62
|
+
* Runs `fn` with the hosted runtime configuration set to `config` (and no runtime file), then
|
|
63
|
+
* restores the previous environment. The override is process-wide: do not run overlapping calls.
|
|
64
|
+
* Refuses to run in a production build.
|
|
65
|
+
*/
|
|
66
|
+
export async function withTestRuntimeConfig(config, fn) {
|
|
67
|
+
if (process.env.NODE_ENV === 'production')
|
|
68
|
+
throw new Error('withTestRuntimeConfig is for tests and local development only');
|
|
69
|
+
const { sessionToken, ...hosted } = artifactRuntimeConfigSchema.parse(config);
|
|
70
|
+
const previous = Object.fromEntries(runtimeEnvironmentKeys.map(key => [key, process.env[key]]));
|
|
71
|
+
delete process.env.SEQUENCE_ARTIFACT_RUNTIME_FILE;
|
|
72
|
+
process.env.SEQUENCE_ARTIFACT_RUNTIME_CONFIG = JSON.stringify(hosted);
|
|
73
|
+
process.env.SEQUENCE_ARTIFACT_RUNTIME_TOKEN = sessionToken;
|
|
74
|
+
try {
|
|
75
|
+
return await fn();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
for (const key of runtimeEnvironmentKeys) {
|
|
79
|
+
const value = previous[key];
|
|
80
|
+
if (value === undefined)
|
|
81
|
+
delete process.env[key];
|
|
82
|
+
else
|
|
83
|
+
process.env[key] = value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
|
-
export { createArtifactServerClient
|
|
3
|
-
export
|
|
2
|
+
export { createArtifactServerClient } from './server/client.js';
|
|
3
|
+
export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './server/errors.js';
|
|
4
|
+
export type { ArtifactAtlasRefusal } from './server/errors.js';
|
|
5
|
+
export type { ArtifactAuditEvent, ArtifactOrmOperations, ArtifactOrmOptions, ArtifactProcessAdvance, ArtifactProcessAdvanceResult, ArtifactProcessArtifactRef, ArtifactProcessAssign, ArtifactProcessAssignResult, ArtifactProcessDefinition, ArtifactProcessDescribeQuery, ArtifactProcessEdge, ArtifactProcessHumanNode, ArtifactProcessNode, ArtifactProcessNodeBase, ArtifactProcessNodeState, ArtifactProcessPrincipal, ArtifactProcessPrincipalSearch, ArtifactProcessPrincipals, ArtifactProcessRun, ArtifactProcessRunRecord, ArtifactProcessRunSnapshot, ArtifactProcessRunsQuery, ArtifactProcessRunViewerChange, ArtifactProcessRunViewers, ArtifactProcessStart, ArtifactProcessStepRecord, ArtifactProcessTask, ArtifactProcessTasksQuery, ArtifactServerAudit, ArtifactServerClient, ArtifactServerClientOptions, ArtifactServerContext, ArtifactServerFetchOptions, ArtifactServerFlags, ArtifactServerProcesses, } from './server/client.js';
|
|
6
|
+
export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
|
|
7
|
+
export type { ArtifactAuditAction, ArtifactProcessPrincipalType, ArtifactProcessRunStatus, ArtifactProcessTaskView, } from './server/broker-contract.js';
|
|
4
8
|
export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
|
|
5
9
|
export type { ArtifactRuntimeConfig, ArtifactRequestHeaders } from './server/runtime-config.js';
|
|
6
|
-
export { getArtifactBrowserBootstrap } from './server/request-context.js';
|
|
7
|
-
export type { ArtifactRequestContext } from './server/request-context.js';
|
|
10
|
+
export { ArtifactContextError, getArtifactBrowserBootstrap } from './server/request-context.js';
|
|
11
|
+
export type { ArtifactContextErrorReason, ArtifactRequestContext, RequestContextV2External } from './server/request-context.js';
|
|
12
|
+
export { verifyArtifactExternalContext } from './server/external-context.js';
|
|
13
|
+
export type { ArtifactExternalAccept } from './server/external-context.js';
|
|
14
|
+
export { externalEvidence } from './server/principal.js';
|
|
15
|
+
export type { ArtifactExternalEvidence, ArtifactExternalPrincipal, ArtifactPrincipal } from './server/principal.js';
|
|
8
16
|
export type { ArtifactLogMetadata, ArtifactLogRecord } from './server/logger.js';
|
|
17
|
+
export type { ArtifactOrmJsonScalars } from './orm-json-scalars.js';
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
|
-
export { createArtifactServerClient
|
|
2
|
+
export { createArtifactServerClient } from './server/client.js';
|
|
3
|
+
export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './server/errors.js';
|
|
4
|
+
export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
|
|
3
5
|
export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
|
|
4
|
-
export { getArtifactBrowserBootstrap } from './server/request-context.js';
|
|
6
|
+
export { ArtifactContextError, getArtifactBrowserBootstrap } from './server/request-context.js';
|
|
7
|
+
export { verifyArtifactExternalContext } from './server/external-context.js';
|
|
8
|
+
export { externalEvidence } from './server/principal.js';
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* on-disk tree the rest of the pipeline (build → upload) already consumes:
|
|
5
5
|
*
|
|
6
6
|
* - local folder (default, unchanged): `deploy [dir]`
|
|
7
|
-
* - a platform git-service repo: `deploy --repo <ns>/<name> [--ref <r>]`
|
|
8
|
-
* - any external git URL: `deploy --git-url <url> [--ref <r>]`
|
|
7
|
+
* - a platform git-service repo: `deploy --repo <ns>/<name> [--ref <r>] [--path <dir>]`
|
|
8
|
+
* - any external git URL: `deploy --git-url <url> [--ref <r>] [--path <dir>]`
|
|
9
9
|
*
|
|
10
10
|
* Remote sources are materialized to a temp dir and torn down after the build.
|
|
11
|
+
* `--path` selects the application directory inside a remote repository.
|
|
11
12
|
*/
|
|
12
13
|
/** Provenance recorded on the deployment row (git_commit/git_branch/git_dirty). */
|
|
13
14
|
export interface SourceProvenance {
|
|
@@ -23,14 +24,18 @@ export type SourceSpec = {
|
|
|
23
24
|
namespace: string;
|
|
24
25
|
name: string;
|
|
25
26
|
ref?: string;
|
|
27
|
+
path?: string;
|
|
26
28
|
} | {
|
|
27
29
|
kind: 'git-url';
|
|
28
30
|
url: string;
|
|
29
31
|
ref?: string;
|
|
32
|
+
path?: string;
|
|
30
33
|
};
|
|
31
34
|
export interface ResolvedSource {
|
|
32
35
|
/** Absolute, canonical (realpath'd) directory the build should run against. */
|
|
33
36
|
dir: string;
|
|
37
|
+
/** `dir` relative to its repository root as a POSIX path; `.` at the root or outside Git. */
|
|
38
|
+
sourceRoot: string;
|
|
34
39
|
/** True for --repo / --git-url materializations (bare tree, needs install). */
|
|
35
40
|
remote: boolean;
|
|
36
41
|
provenance: SourceProvenance;
|
|
@@ -67,5 +72,7 @@ export declare function resolveArtifactSource(spec: SourceSpec, opts?: {
|
|
|
67
72
|
baseUrl?: string;
|
|
68
73
|
token?: string | null;
|
|
69
74
|
}): Promise<ResolvedSource>;
|
|
75
|
+
/** A local source's directory relative to its Git top level, as Atlas reads it from the repository. */
|
|
76
|
+
export declare function localSourceRoot(dir: string): string;
|
|
70
77
|
/** Working-tree git provenance for a local source (current behavior). */
|
|
71
78
|
export declare function localGitMetadata(dir: string): SourceProvenance;
|
package/dist/source-resolver.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { mkdirSync, rmSync } from 'node:fs';
|
|
3
|
-
import { mkdtemp, realpath, rm } from 'node:fs/promises';
|
|
3
|
+
import { mkdtemp, realpath, rm, stat } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join, resolve } from 'node:path';
|
|
5
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { createScrubbedChildEnv } from './child-environment.js';
|
|
7
7
|
import { materializeRepo, resolveCommitSha, resolveRepo } from './git-service-client.js';
|
|
8
8
|
import { gitServiceCloneUrl, resolveGitPatFromEnv, runGitClone } from './git-clone.js';
|
|
@@ -36,6 +36,14 @@ function flagString(flags, key) {
|
|
|
36
36
|
throw new Error(`--${key} requires a value.`);
|
|
37
37
|
return value;
|
|
38
38
|
}
|
|
39
|
+
/** A canonical relative repository directory: no absolute, empty, `.` or `..` segments. */
|
|
40
|
+
function validateRepoPath(path) {
|
|
41
|
+
if (path.trim() !== path || isAbsolute(path) || path.includes('\\') ||
|
|
42
|
+
[...path].some((character) => character.charCodeAt(0) < 0x20) ||
|
|
43
|
+
path.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
44
|
+
throw new Error(`--path must be a canonical relative directory within the repository (got ${JSON.stringify(path)}).`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
39
47
|
/**
|
|
40
48
|
* Derive the source spec from parsed CLI args. Enforces mutual exclusivity:
|
|
41
49
|
* a `[dir]` positional, `--repo`, and `--git-url` are three ways to name the
|
|
@@ -45,6 +53,7 @@ export function parseSourceSpec(input) {
|
|
|
45
53
|
const repo = flagString(input.flags, 'repo');
|
|
46
54
|
const gitUrl = flagString(input.flags, 'git-url');
|
|
47
55
|
const ref = flagString(input.flags, 'ref');
|
|
56
|
+
const path = flagString(input.flags, 'path');
|
|
48
57
|
const dir = input.positional[0];
|
|
49
58
|
if (repo && gitUrl)
|
|
50
59
|
throw new Error('Pass only one of --repo or --git-url.');
|
|
@@ -54,12 +63,17 @@ export function parseSourceSpec(input) {
|
|
|
54
63
|
throw new Error('--ref only applies together with --repo or --git-url.');
|
|
55
64
|
if (ref && ref.startsWith('-'))
|
|
56
65
|
throw new Error(`--ref must not start with "-" (got "${ref}").`);
|
|
66
|
+
if (path !== undefined && !repo && !gitUrl)
|
|
67
|
+
throw new Error('--path only applies together with --repo or --git-url; pass a local [dir] instead.');
|
|
68
|
+
if (path !== undefined)
|
|
69
|
+
validateRepoPath(path);
|
|
70
|
+
const selection = path === undefined ? {} : { path };
|
|
57
71
|
if (repo) {
|
|
58
72
|
const parts = repo.split('/');
|
|
59
73
|
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
60
74
|
throw new Error(`--repo must be "<namespace>/<name>" (got "${repo}").`);
|
|
61
75
|
}
|
|
62
|
-
return { kind: 'git-service', namespace: parts[0], name: parts[1], ref };
|
|
76
|
+
return { kind: 'git-service', namespace: parts[0], name: parts[1], ref, ...selection };
|
|
63
77
|
}
|
|
64
78
|
if (gitUrl) {
|
|
65
79
|
let parsed;
|
|
@@ -76,7 +90,7 @@ export function parseSourceSpec(input) {
|
|
|
76
90
|
parsed.hash) {
|
|
77
91
|
throw new Error(`--git-url must be a credential-free HTTPS URL (got "${redactGitUrl(gitUrl)}").`);
|
|
78
92
|
}
|
|
79
|
-
return { kind: 'git-url', url: gitUrl, ref };
|
|
93
|
+
return { kind: 'git-url', url: gitUrl, ref, ...selection };
|
|
80
94
|
}
|
|
81
95
|
return { kind: 'local', dir: resolve(dir ?? '.') };
|
|
82
96
|
}
|
|
@@ -88,7 +102,7 @@ export function parseSourceSpec(input) {
|
|
|
88
102
|
*/
|
|
89
103
|
export async function resolveArtifactSource(spec, opts = {}) {
|
|
90
104
|
if (spec.kind === 'local') {
|
|
91
|
-
return { dir: spec.dir, remote: false, provenance: localGitMetadata(spec.dir), cleanup: async () => { } };
|
|
105
|
+
return { dir: spec.dir, sourceRoot: localSourceRoot(spec.dir), remote: false, provenance: localGitMetadata(spec.dir), cleanup: async () => { } };
|
|
92
106
|
}
|
|
93
107
|
if (spec.kind === 'git-url' && opts.authMode === 'm2m') {
|
|
94
108
|
throw new Error('M2M/CI builds only accept platform-managed --repo sources; arbitrary --git-url sources are not trusted in CI.');
|
|
@@ -96,6 +110,9 @@ export async function resolveArtifactSource(spec, opts = {}) {
|
|
|
96
110
|
const workspace = await makeTempDir();
|
|
97
111
|
const dest = join(workspace, 'source');
|
|
98
112
|
const cleanup = () => removeDir(workspace);
|
|
113
|
+
const materialized = async (provenance) => ({
|
|
114
|
+
dir: await selectRepoDir({ repoDir: dest, path: spec.path }), sourceRoot: spec.path ?? '.', remote: true, provenance, cleanup,
|
|
115
|
+
});
|
|
99
116
|
try {
|
|
100
117
|
if (spec.kind === 'git-service') {
|
|
101
118
|
if (!opts.authMode || !opts.baseUrl || !opts.token) {
|
|
@@ -117,7 +134,7 @@ export async function resolveArtifactSource(spec, opts = {}) {
|
|
|
117
134
|
if (opts.authMode === 'm2m') {
|
|
118
135
|
const count = await materializeRepo({ baseUrl: opts.baseUrl, token: opts.token, repoId: repo.id, ref: sha, destDir: dest });
|
|
119
136
|
console.log(`[seq-studio] source: ${spec.namespace}/${spec.name}@${ref} (${sha.slice(0, 10)}, ${count} file(s), M2M)`);
|
|
120
|
-
return
|
|
137
|
+
return await materialized({ gitCommit: sha, gitBranch: ref, gitDirty: false });
|
|
121
138
|
}
|
|
122
139
|
// Human path: materialize via a smart-HTTP `git clone` (one pack
|
|
123
140
|
// transfer). The earlier per-blob JSON walk was O(files) serial HTTP
|
|
@@ -129,7 +146,7 @@ export async function resolveArtifactSource(spec, opts = {}) {
|
|
|
129
146
|
const cloneUrl = gitServiceCloneUrl(opts.baseUrl, repo.id);
|
|
130
147
|
await runGitClone({ cloneUrl, destDir: dest, pat, ref: sha });
|
|
131
148
|
console.log(`[seq-studio] source: ${spec.namespace}/${spec.name}@${ref} (${sha.slice(0, 10)}, git clone)`);
|
|
132
|
-
return
|
|
149
|
+
return await materialized({ gitCommit: sha, gitBranch: ref, gitDirty: false });
|
|
133
150
|
}
|
|
134
151
|
throw new Error('Deploying from --repo requires a git PAT. Set ATLAS_GIT_PAT to a token with the ' +
|
|
135
152
|
'repo:read scope (create one in Atlas → Settings → Tokens, or `seq-studio auth pat create`). ' +
|
|
@@ -137,13 +154,43 @@ export async function resolveArtifactSource(spec, opts = {}) {
|
|
|
137
154
|
}
|
|
138
155
|
const { sha, branch } = cloneGitUrl(spec.url, spec.ref, dest);
|
|
139
156
|
console.log(`[seq-studio] source: ${redactGitUrl(spec.url)}${spec.ref ? `@${spec.ref}` : ''} (${sha.slice(0, 10)})`);
|
|
140
|
-
return
|
|
157
|
+
return await materialized({ gitCommit: sha, gitBranch: branch, gitDirty: false });
|
|
141
158
|
}
|
|
142
159
|
catch (error) {
|
|
143
160
|
await cleanup();
|
|
144
161
|
throw error;
|
|
145
162
|
}
|
|
146
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Select `--path` inside a materialized repository. realpath containment
|
|
166
|
+
* prevents a committed symlink from escaping the checkout.
|
|
167
|
+
*/
|
|
168
|
+
async function selectRepoDir({ repoDir, path }) {
|
|
169
|
+
if (path === undefined)
|
|
170
|
+
return repoDir;
|
|
171
|
+
const candidate = resolve(repoDir, path);
|
|
172
|
+
const isDirectory = await stat(candidate).then((entry) => entry.isDirectory(), () => false);
|
|
173
|
+
if (!isDirectory)
|
|
174
|
+
throw new Error(`--path does not name a directory in the repository: ${path}`);
|
|
175
|
+
const [root, selected] = await Promise.all([realpath(repoDir), realpath(candidate)]);
|
|
176
|
+
const contained = relative(root, selected);
|
|
177
|
+
if (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {
|
|
178
|
+
throw new Error(`--path must stay within the repository: ${path}`);
|
|
179
|
+
}
|
|
180
|
+
return selected;
|
|
181
|
+
}
|
|
182
|
+
/** A local source's directory relative to its Git top level, as Atlas reads it from the repository. */
|
|
183
|
+
export function localSourceRoot(dir) {
|
|
184
|
+
try {
|
|
185
|
+
const prefix = String(execFileSync('git', ['rev-parse', '--show-prefix'], {
|
|
186
|
+
cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
187
|
+
})).trim().replace(/\/$/, '');
|
|
188
|
+
return prefix || '.';
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return '.';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
147
194
|
/** Working-tree git provenance for a local source (current behavior). */
|
|
148
195
|
export function localGitMetadata(dir) {
|
|
149
196
|
const gitOpts = {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Sequence development
|
|
2
|
+
|
|
3
|
+
Before every development task, read the live documentation for this project's
|
|
4
|
+
target environment. Find the environment's URL with `seq-studio envs list`
|
|
5
|
+
(or the project configuration), then open `<environment-url>/docs`.
|
|
6
|
+
|
|
7
|
+
Start with that site's `/docs/llms.txt` index and follow the relevant pages as
|
|
8
|
+
needed. The hosted docs are the single source of truth for platform usage;
|
|
9
|
+
do not duplicate their instructions in this file. If docs access is blocked,
|
|
10
|
+
open the same URL in the signed-in browser.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -77,6 +77,94 @@ platform-user relations also read `<namespace>/platformUsers`). Under
|
|
|
77
77
|
`data.write`, use `<namespace>/ops/<Operation>` for each persisted mutation the
|
|
78
78
|
app runs, or `<namespace>/<table>` for table-level coverage.
|
|
79
79
|
|
|
80
|
+
To start a Lattice process, list its id under `processes.start` and call
|
|
81
|
+
`processes.start({ process, runId, title, entity, metadata, assignments })`
|
|
82
|
+
from a server action or route handler. The run starts as the signed-in user (or
|
|
83
|
+
the attached service account, through `asService()`) and needs that identity's
|
|
84
|
+
Invoker grant on the process. `runId` is an idempotency key: starting the same
|
|
85
|
+
id again returns the existing run. `metadata` is the run's input
|
|
86
|
+
(`ctx.run.metadata`). `assignments` names who works each human node
|
|
87
|
+
(`{ execute: { groups: ['<group id>'] } }`), as `POST /api/lattice/runs` takes
|
|
88
|
+
it; Atlas refuses principals it has tombstoned. The start node and version stay
|
|
89
|
+
with the process.
|
|
90
|
+
|
|
91
|
+
To read a process's runs, list its id under `processes.read` and call
|
|
92
|
+
`processes.listRuns({ process, status, entity, limit, archived })` or
|
|
93
|
+
`processes.getRun(runId)`. Both answer as `GET /api/lattice/runs` and
|
|
94
|
+
`GET /api/lattice/runs/[runId]` would for the signed-in user: the list holds
|
|
95
|
+
only runs that user may view, `getRun` needs Viewer on the run and returns
|
|
96
|
+
`{ run, steps, states, event_cursor }`, and `run.metadata` is blank on read for
|
|
97
|
+
every viewer — keep the data your pages need in your own ORM namespace. Reads
|
|
98
|
+
work from pages and GET route handlers; starting a run does not.
|
|
99
|
+
|
|
100
|
+
The same `processes.read` declaration lets you render the process itself.
|
|
101
|
+
`processes.describe({ process, version })` returns the applied template a run
|
|
102
|
+
is filled in from — `{ id, name, version, new_runs_disabled, start_node_id,
|
|
103
|
+
input_schema, nodes }` — for the active version, or for a run's
|
|
104
|
+
`processVersion` to show the template that run started from. Every node lists
|
|
105
|
+
its `id`, `kind`, `title`, `description` and `outgoing_edges`; a `human` node
|
|
106
|
+
also carries what a person working it is shown: `advance_edges`,
|
|
107
|
+
`complete_label`, the author's `metadata` bag (form fields, surface hints) and
|
|
108
|
+
`comments_enabled`. Automation, agent and function internals are the process's
|
|
109
|
+
own and never appear. It needs Viewer on the process, which anyone who may
|
|
110
|
+
start it has. `processes.listTasks({ processes, view, limit })` lists the
|
|
111
|
+
signed-in user's own human steps across your declared processes (a subset, or
|
|
112
|
+
all of them) — `view: 'active'` (default) for steps waiting on them,
|
|
113
|
+
`'completed'` for ones they finished — each with its run and node identity plus
|
|
114
|
+
the node's `title`, interpolated `description`, `advance_edges`,
|
|
115
|
+
`complete_label` and `metadata`. It is always the caller's queue: there is no
|
|
116
|
+
way to ask for someone else's, and nothing in a task lets you advance it
|
|
117
|
+
without the user's own Lattice authority.
|
|
118
|
+
|
|
119
|
+
To work those steps, list the process id under `processes.act` as well and
|
|
120
|
+
call, from a server action or a non-GET route handler:
|
|
121
|
+
`processes.advance({ runId, stepId, edgeId, payload })` completes one of the
|
|
122
|
+
signed-in user's parked steps — `edgeId` is one of the node's `advance_edges`
|
|
123
|
+
(omit it on a terminal node), `payload` is the step's output, and `stepId`
|
|
124
|
+
disambiguates when a parallel block has several human branches parked. It
|
|
125
|
+
needs the user's own `advancer` grant on that step: the application cannot
|
|
126
|
+
complete a step as someone else. `processes.assign({ runId, stepId, users,
|
|
127
|
+
groups })` reassigns a parked step (its advancers become exactly those
|
|
128
|
+
principals) and `processes.grantViewers` / `processes.revokeViewers({ runId,
|
|
129
|
+
users, groups })` share a run or take a share back; all three need the user to
|
|
130
|
+
own the run, and `processes.listViewers(runId)` shows the run's stored shares.
|
|
131
|
+
Users are Auth0 subs and groups are chat-group ids — take them from
|
|
132
|
+
`processes.searchPrincipals({ q, types, limit })`, which finds people in the
|
|
133
|
+
user's own tenant and groups they can see, as the Lattice picker does. The
|
|
134
|
+
same applies through `asService()`: the attached service account acts under its
|
|
135
|
+
own grants, with no elevation.
|
|
136
|
+
|
|
137
|
+
A refused process call rejects with an `ArtifactServerError` whose `status` is
|
|
138
|
+
Atlas's and whose `atlas` field keeps what Atlas said: `atlas.detail` is the
|
|
139
|
+
reason, `atlas.code` the route's machine code when it has one — an advance
|
|
140
|
+
answers `RUN_TERMINAL` (410) once the run has finished, `WORKFLOW_NOT_RUNNING`
|
|
141
|
+
(409) while a fresh run's workflow is still being dispatched (retry shortly),
|
|
142
|
+
`NO_PARKED_STEP` and `EDGE_ID_MISMATCH` (409) when the step moved on or was
|
|
143
|
+
completed another way. Branch on `atlas.code`, not on the message text.
|
|
144
|
+
|
|
145
|
+
Lattice calls this server only at paths listed under `server.lattice.accept`.
|
|
146
|
+
Each entry is an exact origin-relative path (`/api/hooks/lattice`), with no
|
|
147
|
+
wildcards. A process node names that path; the caller needs `end_user` on this
|
|
148
|
+
app. The signed request context includes the Lattice run, node, step, and
|
|
149
|
+
process ids.
|
|
150
|
+
|
|
151
|
+
Feature flags need no declaration. `flags.isEnabled(name)` (or
|
|
152
|
+
`flags.evaluate(names)` for up to 32 at once) asks Atlas, which evaluates them
|
|
153
|
+
for the signed-in user as its own routes do, from any route, page or action. A
|
|
154
|
+
refused call rejects, so treat a failure as off where a flag gates a change.
|
|
155
|
+
|
|
156
|
+
To keep an audit trail of what users do in the application, declare
|
|
157
|
+
`audit: { service: artifact_studio }` under `server.capabilities` and call
|
|
158
|
+
`audit.record({ action, detail, resources, metadata })` from the server, for
|
|
159
|
+
example `{ action: 'view', detail: 'invoices.view', resources: [{ type:
|
|
160
|
+
'invoice', id }] }`. Atlas records it for the signed-in user, adds where it
|
|
161
|
+
came from (project, deployment, request) and resolves once the audit log has it.
|
|
162
|
+
Reads may record from GET handlers and pages. An application that replaces a
|
|
163
|
+
product's Atlas routes can record under that product's audit service once
|
|
164
|
+
Atlas registers the project for it. `metadata` takes scalars that say what
|
|
165
|
+
happened, such as counts, outcomes and what was searched for. Never put request
|
|
166
|
+
bodies, document contents or secrets in it.
|
|
167
|
+
|
|
80
168
|
Managed secrets stay in hosted runtimes. For local integration tests, use your
|
|
81
169
|
own test credentials, mocks or an existing governed function. Use a hosted
|
|
82
170
|
preview for application code that needs a managed secret. Never put secrets in
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"dev": "next dev --webpack --hostname 127.0.0.1",
|
|
11
11
|
"build": "next build",
|
|
12
12
|
"start": "next start --hostname 127.0.0.1",
|
|
13
|
-
"
|
|
13
|
+
"lint": "next typegen && tsc --noEmit --noUnusedLocals --noUnusedParameters --noFallthroughCasesInSwitch",
|
|
14
|
+
"type-check": "next typegen && tsc --noEmit",
|
|
15
|
+
"test": "node --test tests/*.test.mjs"
|
|
14
16
|
},
|
|
15
17
|
"dependencies": {
|
|
16
18
|
"@sequenceholdings/artifact-studio": "{{artifactStudioVersion}}",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { greet } from '../app/actions.ts'
|
|
4
|
+
import { GET } from '../app/api/health/route.ts'
|
|
5
|
+
|
|
6
|
+
test('greeting validates input and returns the server action result', async () => {
|
|
7
|
+
const data = new FormData()
|
|
8
|
+
data.set('name', ' Atlas ')
|
|
9
|
+
assert.deepEqual(await greet({ message: '', error: null }, data), {
|
|
10
|
+
message: 'Hello, Atlas. Your server action ran successfully.', error: null,
|
|
11
|
+
})
|
|
12
|
+
for (const name of ['', ' '.repeat(3), 'a'.repeat(81)]) {
|
|
13
|
+
data.set('name', name)
|
|
14
|
+
const result = await greet({ message: '', error: null }, data)
|
|
15
|
+
assert.equal(result.message, '')
|
|
16
|
+
assert.equal(result.error, 'Enter a name between 1 and 80 characters.')
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('health route returns uncached JSON without any runtime bindings', async () => {
|
|
21
|
+
const response = GET()
|
|
22
|
+
assert.equal(response.status, 200)
|
|
23
|
+
assert.equal(response.headers.get('cache-control'), 'no-store')
|
|
24
|
+
assert.deepEqual(await response.json(), { ok: true, framework: 'nextjs' })
|
|
25
|
+
})
|
|
@@ -1,90 +1,10 @@
|
|
|
1
|
-
# Sequence
|
|
1
|
+
# Sequence development
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Before every development task, read the live documentation for this project's
|
|
4
|
+
target environment. Find the environment's URL with `seq-studio envs list`
|
|
5
|
+
(or the project configuration), then open `<environment-url>/docs`.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import { unwrap, seq } from './lib/api'
|
|
10
|
-
const customer = await unwrap<Customer>(seq.api.get(`/api/crm/customers/${id}`))
|
|
11
|
-
```
|
|
12
|
-
For endpoints with extra envelope fields (pagination `meta`, warnings), drop to `seq.api.fetch` and read `res.body` directly.
|
|
13
|
-
|
|
14
|
-
2. **FormData blocker.** `seq.api.fetch` `JSON.stringify`s any non-string `body` value, which silently turns `FormData` into `"{}"`. For multipart uploads, use `seq.api.upload(path, file, fields?, options?)`; the bridge constructs `FormData` in the trusted host and returns the usual `{ ok, status, body }` envelope.
|
|
15
|
-
|
|
16
|
-
3. **Capability matcher: exact paths, `/*` prefix wildcards, and single-segment `*` patterns.** `{id}` template forms do **not** match. List both the bare path and the prefix wildcard for broad resources, or use a least-privilege segment pattern for dynamic routes:
|
|
17
|
-
```yaml
|
|
18
|
-
- /api/crm/customers
|
|
19
|
-
- /api/crm/customers/*
|
|
20
|
-
- /api/lattice/runs/*/advance
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Prerequisite
|
|
24
|
-
|
|
25
|
-
None — `seq-studio artifact init <dir>` works from any directory; no monorepo checkout is required. The template lists `@sequenceholdings/artifact-studio` and `@sequenceholdings/atlas-ui` in **devDependencies** for local editor/type-check tooling only. `seq-studio artifact build|deploy` bundles against the CLI's own pinned copies of `react`, `react-dom`, `@tanstack/react-query`, `react-router-dom`, `sonner`, and `@sequenceholdings/atlas-ui`, and resolves `@sequenceholdings/artifact-studio` to the CLI-pinned SDK, whose `seq` export uses the host-injected `window.seq`. The SDK also provides `createArtifactOrmExecutor({ namespace, manifest })` for persisted ORM operations; declare `/api/data/v2/<namespace>` under `capabilities.api.write`, including queries (execution uses POST). The helper handles the ORM envelope; do not pass its result to the CRM `unwrap()` helper. Remote deploys (`--repo` / `--git-url`) run `pnpm install --prod` for **app-specific** `dependencies` only (e.g. `recharts`, `reactflow`) from the artifact lockfile.
|
|
26
|
-
|
|
27
|
-
## Package manager
|
|
28
|
-
|
|
29
|
-
Use **pnpm** for artifacts you will deploy from git — `seq-studio artifact build|deploy --repo/--git-url` runs `pnpm install --frozen-lockfile --prod` when `package.json` is present, so commit `pnpm-lock.yaml`. Local editor-only installs can use another package manager, but pnpm keeps Sequence's `minimumReleaseAge` supply-chain quarantine in effect (see the studio-cli README). If you have a stale checkout from the old `file:`-link era, `rm -rf node_modules` and reinstall.
|
|
30
|
-
|
|
31
|
-
## Key files
|
|
32
|
-
|
|
33
|
-
| File | Role |
|
|
34
|
-
|---|---|
|
|
35
|
-
| `artifact.bundle.yml` | Capability manifest — every API path the artifact calls: exact paths, `/*` prefix wildcards, or single-segment `*` patterns |
|
|
36
|
-
| `src/main.tsx` | Providers: `QueryClientProvider` → `PortalContainerProvider` → `App` |
|
|
37
|
-
| `src/App.tsx` | Entrypoint UI |
|
|
38
|
-
| `src/lib/api.ts` | `unwrap<T>` envelope helper + re-export of `seq` |
|
|
39
|
-
| `src/styles.css` | Tailwind v4 + canonical `@sequenceholdings/atlas-ui/tokens.css` |
|
|
40
|
-
| `vite.config.ts` | Vite + React + Tailwind v4 plugin |
|
|
41
|
-
|
|
42
|
-
## Defaults
|
|
43
|
-
|
|
44
|
-
- UI: prefer `@sequenceholdings/atlas-ui` primitives (`Button`, `Card`, `Input`, `Dialog`, `Select`, `Checkbox`, `DropdownMenu`, `KpiTile`, `Switch`, …) over raw Tailwind. The design-system cutover made `Select`, `Checkbox`, `DropdownMenu`, `Command`, `Calendar`, `ScrollArea`, `Table`, and the Task family first-class exports — use them directly; fall back to `Popover` compositions or native controls only when a component is genuinely missing.
|
|
45
|
-
- Routing: `HashRouter`. Mount routes at `/`, not at the Atlas app prefix.
|
|
46
|
-
- Data: React Query is wired in `src/main.tsx`. Use `useQuery` / `useMutation` against `unwrap(seq.api.*)`.
|
|
47
|
-
- Icons: do **not** add third-party deps like `@tabler/icons-react`. Deploys from git (`--repo` / `--git-url`) build a bare tree — `seq-studio` runs `pnpm install --prod` from your lockfile, then aliases platform peers (`react`, `react-dom`, `@tanstack/react-query`, `react-router-dom`, `sonner`, `@sequenceholdings/atlas-ui`). List only app-specific imports (e.g. `recharts`) in `dependencies`; do not add file/workspace `@sequenceholdings/*` specs in git-service repos, because remote installs only accept registry semver specs. Inline SVG components in `src/icons.tsx`.
|
|
48
|
-
|
|
49
|
-
## Tokens — colour comes from semantic roles, never a raw ramp
|
|
50
|
-
|
|
51
|
-
This app bundles `@sequenceholdings/atlas-ui/tokens.css` and re-themes per tenant for free. Reach for a **semantic role** and colour follows the host brand and light/dark automatically.
|
|
52
|
-
|
|
53
|
-
**Never** use the numbered `black-*` / `white-*` opacity scales (`text-black-40`, `bg-black-6`, `border-black-10`), raw Tailwind neutrals (`gray|slate|zinc|neutral|stone-N`), arbitrary hex in class strings (`bg-[#1f2937]`), or `var(--black-*)` / `var(--color-black-*)`. These are **retired** — post-cutover they resolve to nothing or to a fixed ink that won't flip in dark. Bare `text-white` / `bg-black` and opacity modifiers like `bg-foreground/10` stay legal; only the numbered steps and raw palettes are banned. There is no old→new lookup table on purpose — pick by *what the element is and what it sits on*.
|
|
54
|
-
|
|
55
|
-
| Situation | Role |
|
|
56
|
-
|---|---|
|
|
57
|
-
| The page surface | `bg-background` — the page only, never a panel |
|
|
58
|
-
| Bordered/rounded panel, card, list | `bg-card` — a border is not a surface; every bordered container needs an explicit surface |
|
|
59
|
-
| Inset well inside a card (code block, read-only value) | `bg-muted` |
|
|
60
|
-
| Form field (input, textarea, select trigger) | `bg-field` |
|
|
61
|
-
| Interactive / hover fill | `bg-accent` / `bg-surface-hover` |
|
|
62
|
-
| Selected / active / focus state fill | `bg-foreground/8`–`bg-foreground/15` overlay |
|
|
63
|
-
| Modal scrim behind a dialog | `bg-overlay` — never `bg-foreground/N` (washes lighter in dark) |
|
|
64
|
-
| Primary text | `text-foreground` |
|
|
65
|
-
| Resting nav items, meta rows | `text-foreground-secondary` |
|
|
66
|
-
| Section labels, captions | `text-muted-foreground` |
|
|
67
|
-
| Standard border | `border-border` (form controls: `border-input`) |
|
|
68
|
-
|
|
69
|
-
Surfaces **nest** — never repeat a rung (`bg-card` inside `bg-card` is invisible): `bg-background` (page) → `bg-card` (panel, + border + `shadow-xs` to lift) → `bg-muted` (inset well).
|
|
70
|
-
|
|
71
|
-
**Dual-runtime.** Inside Atlas the artifact runs in its own iframe and inherits nothing — the runtime posts the host's resolved brand in as inline custom properties (theme channel + `data-tokens`). Standalone `vite dev` has no host, so the artifact must still render on the base semantic roles. **Do not** bundle `tokens.v2.tenants.css`, hardcode an opco's brand, or add `[data-theme]` selectors — that pins the artifact to one tenant.
|
|
72
|
-
|
|
73
|
-
## Bridge methods
|
|
74
|
-
|
|
75
|
-
Two primitives on `seq.api`:
|
|
76
|
-
|
|
77
|
-
- `seq.api.fetch(path, options?)` returns `{ ok, status, body }` — does not throw on non-2xx. Use when you need the status (e.g. handle 409) or the raw envelope with extra fields like `meta`.
|
|
78
|
-
- `seq.api.request(method, path, body?)` throws on non-2xx and returns parsed JSON. Sugar wrappers `seq.api.get / .post / .patch / .delete` are thin shells around `request`, so they also throw.
|
|
79
|
-
|
|
80
|
-
Wrap reads/writes that should auto-throw through `unwrap()` in `src/lib/api.ts`. Drop to `seq.api.fetch` when you need pagination `meta` or status inspection.
|
|
81
|
-
|
|
82
|
-
## Lattice embed context
|
|
83
|
-
|
|
84
|
-
When the artifact is served from a Lattice human node (the **Tasks/Inbox** task surface, or the builder Artifacts tab), `seq.lattice.context()` tells you which run/node/step you're rendering for:
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
const { runId, nodeId, stepId, processId } = seq.lattice.context()
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
It's **synchronous** (the host injects this into the sandbox up front — no bridge round trip) and each field is `null` when unavailable. The Inbox provides all four; the builder Artifacts tab has no live `stepId`; a standalone open has no context at all. Use it instead of hand-parsing `window.location` — that's the sanctioned, stable accessor, and you don't need to declare anything in `artifact.bundle.yml` for it. To read step-scoped data, pass `runId`/`stepId` to the relevant Atlas endpoint (which still authorizes the caller server-side).
|
|
7
|
+
Start with that site's `/docs/llms.txt` index and follow the relevant pages as
|
|
8
|
+
needed. The hosted docs are the single source of truth for platform usage;
|
|
9
|
+
do not duplicate their instructions in this file. If docs access is blocked,
|
|
10
|
+
open the same URL in the signed-in browser.
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
# Sequence Artifact Studio app
|
|
2
|
-
|
|
3
|
-
Agent instructions for this artifact live in `AGENTS.md` — one file every coding agent reads
|
|
4
|
-
(Cursor and Codex load `AGENTS.md` natively; the import below pulls it in for Claude Code).
|
|
5
|
-
Add or edit guidance in `AGENTS.md`, not here.
|
|
6
|
-
|
|
7
1
|
@AGENTS.md
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@sequenceholdings/artifact-studio": "^0.
|
|
12
|
+
"@sequenceholdings/artifact-studio": "^0.6.0",
|
|
13
13
|
"@sequenceholdings/atlas-ui": "^3.1.1",
|
|
14
14
|
"@tailwindcss/postcss": "^4.2.2",
|
|
15
15
|
"@tailwindcss/vite": "^4.2.2",
|