@sequenceholdings/artifact-studio 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +35 -1
  2. package/VERSION-PIN.md +9 -0
  3. package/dist/active-deploy-policy.d.ts +9 -3
  4. package/dist/active-deploy-policy.js +20 -8
  5. package/dist/artifact-env-bindings.d.ts +9 -0
  6. package/dist/artifact-env-bindings.js +32 -0
  7. package/dist/build.js +6 -0
  8. package/dist/cli.js +61 -12
  9. package/dist/deployment-validation.d.ts +2 -0
  10. package/dist/deployment-validation.js +2 -0
  11. package/dist/dev-link.js +28 -2
  12. package/dist/env-binding-scan.d.ts +10 -0
  13. package/dist/env-binding-scan.js +179 -0
  14. package/dist/env-bindings.d.ts +52 -0
  15. package/dist/env-bindings.js +180 -0
  16. package/dist/gateway/control.d.ts +33 -0
  17. package/dist/gateway/control.js +29 -0
  18. package/dist/gateway/hosted-proxy.d.ts +5 -0
  19. package/dist/gateway/hosted-proxy.js +4 -1
  20. package/dist/gateway/lattice-invoke.d.ts +48 -0
  21. package/dist/gateway/lattice-invoke.js +245 -0
  22. package/dist/gateway/lattice-upstream.d.ts +38 -0
  23. package/dist/gateway/lattice-upstream.js +136 -0
  24. package/dist/gateway/server.js +2 -3
  25. package/dist/gateway/session.d.ts +3 -1
  26. package/dist/gateway/session.js +10 -3
  27. package/dist/manifest-next.d.ts +55 -0
  28. package/dist/manifest-next.js +35 -2
  29. package/dist/manifest-static.d.ts +35 -0
  30. package/dist/manifest-static.js +12 -0
  31. package/dist/manifest.d.ts +48 -0
  32. package/dist/next-build.d.ts +2 -1
  33. package/dist/next-build.js +12 -7
  34. package/dist/offline/cache.d.ts +4 -0
  35. package/dist/offline/cache.js +7 -0
  36. package/dist/offline/connector.d.ts +26 -0
  37. package/dist/offline/connector.js +118 -0
  38. package/dist/offline/crypto.d.ts +5 -0
  39. package/dist/offline/crypto.js +24 -0
  40. package/dist/offline/index.d.ts +8 -0
  41. package/dist/offline/index.js +7 -0
  42. package/dist/offline/indexed-db.d.ts +10 -0
  43. package/dist/offline/indexed-db.js +104 -0
  44. package/dist/offline/outbox.d.ts +7 -0
  45. package/dist/offline/outbox.js +46 -0
  46. package/dist/offline/replay.d.ts +25 -0
  47. package/dist/offline/replay.js +192 -0
  48. package/dist/offline/secure-store.d.ts +6 -0
  49. package/dist/offline/secure-store.js +51 -0
  50. package/dist/offline/types.d.ts +162 -0
  51. package/dist/offline/types.js +1 -0
  52. package/dist/orm-json-scalars.d.ts +21 -0
  53. package/dist/orm-json-scalars.js +21 -0
  54. package/dist/runtime-contracts.d.ts +7 -7
  55. package/dist/sdk-orm.d.ts +7 -1
  56. package/dist/sdk-orm.js +12 -5
  57. package/dist/sdk.d.ts +126 -0
  58. package/dist/server/broker-contract.d.ts +191 -5
  59. package/dist/server/broker-contract.js +142 -1
  60. package/dist/server/client.d.ts +60 -12
  61. package/dist/server/client.js +143 -20
  62. package/dist/server/errors.d.ts +26 -0
  63. package/dist/server/errors.js +29 -0
  64. package/dist/server/managed-server-path.d.ts +6 -0
  65. package/dist/server/managed-server-path.js +14 -0
  66. package/dist/server/processes.d.ts +290 -0
  67. package/dist/server/processes.js +56 -0
  68. package/dist/server/request-context.d.ts +6 -0
  69. package/dist/server/request-context.js +10 -1
  70. package/dist/server.d.ts +5 -1
  71. package/dist/server.js +1 -0
  72. package/dist/source-resolver.d.ts +9 -2
  73. package/dist/source-resolver.js +55 -8
  74. package/dist/templates/nextjs/AGENTS.md +10 -0
  75. package/dist/templates/nextjs/CLAUDE.md +1 -0
  76. package/dist/templates/nextjs/README.md +88 -0
  77. package/dist/templates/react-vite/AGENTS.md +8 -88
  78. package/dist/templates/react-vite/CLAUDE.md +0 -6
  79. package/dist/templates/react-vite/package.json +1 -1
  80. package/dist/templates/react-vite/src/App.tsx +37 -14
  81. package/dist/templates/react-vite/src/styles.css +10 -0
  82. package/package.json +7 -3
  83. package/templates/nextjs/AGENTS.md +10 -0
  84. package/templates/nextjs/CLAUDE.md +1 -0
  85. package/templates/nextjs/README.md +88 -0
  86. package/templates/react-vite/AGENTS.md +8 -88
  87. package/templates/react-vite/CLAUDE.md +0 -6
  88. package/templates/react-vite/package.json +1 -1
  89. package/templates/react-vite/src/App.tsx +37 -14
  90. package/templates/react-vite/src/styles.css +10 -0
@@ -1,26 +1,31 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { StorageApiError, storageClient, storageErrorSchema } from '@sequenceholdings/storage';
2
3
  import { OrmOperationError } from '@sequenceholdings/orm/client';
3
4
  import { z } from 'zod';
4
5
  import { artifactServerRequestSchema } from './broker-contract.js';
6
+ import { ArtifactServerError, platformRequestError } from './errors.js';
7
+ import { createArtifactProcessesClient } from './processes.js';
8
+ import { assertJsonScalarsAcknowledged, ORM_JSON_SCALARS_HEADER } from '../orm-json-scalars.js';
5
9
  import { createArtifactLogger } from './logger.js';
6
10
  import { ARTIFACT_REQUEST_CONTEXT_HEADER, ARTIFACT_RUNTIME_TOKEN_HEADER, ARTIFACT_SERVER_REQUEST_PATH, verifyArtifactRequestContext } from './request-context.js';
7
11
  import { artifactRuntimeConfigSchema, readArtifactRuntimeConfig } from './runtime-config.js';
8
- export class ArtifactServerError extends Error {
9
- status;
10
- code;
11
- constructor(status, code, message) {
12
- super(message);
13
- this.status = status;
14
- this.code = code;
15
- this.name = 'ArtifactServerError';
16
- }
17
- }
12
+ export { ArtifactServerError } from './errors.js';
18
13
  const graphqlResponseSchema = z.object({
19
14
  data: z.unknown().optional(),
20
15
  errors: z.array(z.object({ message: z.string(), extensions: z.object({ code: z.string().optional() }).passthrough().optional() })).optional(),
21
16
  });
17
+ const flagsResponseSchema = z.object({ flags: z.record(z.string(), z.boolean()) });
22
18
  const responseHeaders = ['content-type', 'content-disposition', 'content-length', 'retry-after'];
23
19
  const maxBodyBytes = 1024 * 1024;
20
+ /** A read whose connection failed is sent at most three times, pausing up to 100 ms, then up to 200 ms. */
21
+ const reconnect = { attempts: 3, baseDelayMs: 100 };
22
+ /**
23
+ * Socket and DNS errors after which Atlas never answered, and which clear by themselves: the
24
+ * route to Atlas was briefly gone (an egress NAT out of ports drops new connections, which Node
25
+ * reports as EHOSTUNREACH), Atlas's edge refused or reset the connection, or a lookup failed
26
+ * transiently. Timeouts aren't here: a call that waited out its timeout shouldn't wait it out again.
27
+ */
28
+ const transientConnectionCodes = new Set(['ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'ENETUNREACH', 'EADDRNOTAVAIL', 'EPIPE', 'EAI_AGAIN', 'UND_ERR_SOCKET']);
24
29
  /** One instance per incoming request. Never cache it at module scope. */
25
30
  export async function createArtifactServerClient({ headers, runtime, fetch: fetcher = globalThis.fetch, method, path, logSink }) {
26
31
  const config = runtime ? artifactRuntimeConfigSchema.parse(runtime) : await readArtifactRuntimeConfig();
@@ -41,11 +46,12 @@ export async function createArtifactServerClient({ headers, runtime, fetch: fetc
41
46
  }
42
47
  function buildClient(args) {
43
48
  const { context, mode, logSink } = args;
44
- const request = createBrokerTransport(args);
49
+ const log = createArtifactLogger({ context, sink: logSink });
50
+ const request = createBrokerTransport({ ...args, log });
45
51
  const json = async (operation, signal) => {
46
- const response = await request(operation, signal);
52
+ const response = await request(operation, { signal });
47
53
  if (!response.ok)
48
- throw new ArtifactServerError(response.status, 'platform_request_failed', `Atlas rejected the ${operation.operation} request (${response.status})`);
54
+ throw await platformRequestError({ response, operation: operation.operation });
49
55
  return response.status === 204 ? undefined : response.json();
50
56
  };
51
57
  // storageClient retries replay-safe calls on 503 permissions_pending / storage_busy, so Storage
@@ -56,10 +62,18 @@ function buildClient(args) {
56
62
  throw await storageApiError(response);
57
63
  return response.status === 204 ? undefined : response.json();
58
64
  };
65
+ const evaluateFlags = async (flagNames) => {
66
+ const names = [...new Set(flagNames)];
67
+ const parsed = flagsResponseSchema.safeParse(await json(artifactServerRequestSchema.parse({ operation: 'flags', mode, names })));
68
+ if (!parsed.success)
69
+ throw new ArtifactServerError(502, 'invalid_response', 'Atlas returned a malformed flags response');
70
+ // A flag Atlas didn't answer reads off rather than undefined.
71
+ return Object.fromEntries(names.map(name => [name, parsed.data.flags[name] === true]));
72
+ };
59
73
  return {
60
74
  context,
61
75
  mode,
62
- log: createArtifactLogger({ context, sink: logSink }),
76
+ log,
63
77
  api: createApiClient({ mode, request, json }),
64
78
  storage: storageClient({ request: (method, path, body) => storageJson(artifactServerRequestSchema.parse({ operation: 'storage', mode, method, path, body })) }),
65
79
  functions: {
@@ -68,18 +82,28 @@ function buildClient(args) {
68
82
  return await json(artifactServerRequestSchema.parse({ operation: 'function', mode, id, params }));
69
83
  },
70
84
  },
71
- orm: ({ namespace, manifest }) => ({
85
+ processes: createArtifactProcessesClient({ mode, json }),
86
+ flags: {
87
+ evaluate: evaluateFlags,
88
+ isEnabled: async (flagName) => (await evaluateFlags([flagName]))[flagName] === true,
89
+ },
90
+ audit: { record: (event) => recordAuditEvent({ request, mode, event }) },
91
+ orm: ({ namespace, manifest, jsonScalars }) => ({
72
92
  async execute(op, variables = {}) {
73
- const hash = op.startsWith('sha256:') ? op : manifest.operations[op]?.hash;
93
+ const listed = op.startsWith('sha256:') ? Object.values(manifest.operations).find(entry => entry.hash === op) : manifest.operations[op];
94
+ const hash = op.startsWith('sha256:') ? op : listed?.hash;
74
95
  if (!hash)
75
96
  throw new ArtifactServerError(400, 'unknown_orm_operation', 'ORM operation is not in the generated manifest; run seq-studio orm generate');
76
- const response = await request(artifactServerRequestSchema.parse({ operation: 'orm', mode, namespace, op: hash, variables }));
97
+ const response = await request(artifactServerRequestSchema.parse({ operation: 'orm', mode, namespace, op: hash, variables, ...(jsonScalars ? { jsonScalars } : {}) }),
98
+ // Only an operation the manifest lists as a query is known to change nothing.
99
+ { replaySafe: listed?.kind === 'query' });
77
100
  const parsed = graphqlResponseSchema.safeParse(await response.json().catch(() => null));
78
101
  if (!parsed.success)
79
102
  throw new OrmOperationError('Malformed Data API response', response.status, 'invalid_response');
80
103
  const first = parsed.data.errors?.[0];
81
104
  if (!response.ok || first)
82
105
  throw new OrmOperationError(first?.message ?? `Data API request failed (${response.status})`, response.status, first?.extensions?.code ?? null, parsed.data.errors);
106
+ assertJsonScalarsAcknowledged({ requested: jsonScalars, acknowledged: response.headers.get(ORM_JSON_SCALARS_HEADER), status: response.status });
83
107
  if (parsed.data.data === undefined || parsed.data.data === null)
84
108
  throw new OrmOperationError('Malformed Data API response: missing data', response.status, 'invalid_response');
85
109
  // Mirrors the generated ORM executor contract: consumers provide their generated result type.
@@ -94,13 +118,90 @@ function buildClient(args) {
94
118
  },
95
119
  };
96
120
  }
121
+ /**
122
+ * Sends one audit event, and once more with the same event id when Atlas couldn't confirm delivery
123
+ * (503, or no answer): Atlas derives the stored event id from it and the request, so the audit sink
124
+ * keeps one copy whichever attempt landed.
125
+ */
126
+ async function recordAuditEvent({ request, mode, event }) {
127
+ const { eventId = randomUUID(), ...rest } = event;
128
+ const operation = artifactServerRequestSchema.parse({ operation: 'audit', mode, eventId, ...rest });
129
+ let failure;
130
+ for (let attempt = 0; attempt < 2; attempt++) {
131
+ let response;
132
+ try {
133
+ response = await request(operation);
134
+ }
135
+ catch (error) {
136
+ // A local refusal (expired context, oversized body) won't change on a retry.
137
+ if (error instanceof ArtifactServerError)
138
+ throw error;
139
+ failure = new ArtifactServerError(503, 'audit_unavailable', 'Atlas did not answer the audit request');
140
+ continue;
141
+ }
142
+ await response.body?.cancel();
143
+ if (response.ok)
144
+ return;
145
+ failure = new ArtifactServerError(response.status, response.status === 503 ? 'audit_unavailable' : 'platform_request_failed', `Atlas rejected the audit request (${response.status})`);
146
+ if (response.status !== 503)
147
+ break;
148
+ }
149
+ throw failure;
150
+ }
97
151
  /** Mirrors createStorageClient: Storage's `{ error: { code, message } }` envelope survives the broker. */
98
152
  async function storageApiError(response) {
99
153
  const envelope = storageErrorSchema.safeParse(await response.json().catch(() => undefined));
100
154
  return new StorageApiError(response.status, envelope.success ? envelope.data.error.code : 'request_failed', envelope.success ? envelope.data.error.message : `Storage request failed (${response.status})`);
101
155
  }
102
- function createBrokerTransport({ context, token, getRuntime, fetcher }) {
103
- return async (input, signal) => {
156
+ /**
157
+ * Broker operations that change nothing in Atlas. An ORM call's caller decides from its manifest
158
+ * kind; function invokes and process starts can act, and audit delivery resends on its own terms.
159
+ */
160
+ function isReadOperation(operation) {
161
+ switch (operation.operation) {
162
+ case 'api':
163
+ case 'storage':
164
+ return operation.method === 'GET' || operation.method === 'HEAD';
165
+ case 'flags':
166
+ return true;
167
+ default:
168
+ return false;
169
+ }
170
+ }
171
+ /**
172
+ * The transient connection code behind a failed fetch, or null. Node's fetch throws
173
+ * `TypeError: fetch failed` with the socket error as its cause. When every address of the host
174
+ * failed to connect, that cause is an AggregateError carrying the first address's code.
175
+ */
176
+ function transientConnectionCode(error) {
177
+ let current = error;
178
+ for (let depth = 0; depth < 8 && typeof current === 'object' && current !== null; depth++) {
179
+ if ('code' in current && typeof current.code === 'string' && transientConnectionCodes.has(current.code))
180
+ return current.code;
181
+ current = 'cause' in current ? current.cause : undefined;
182
+ }
183
+ return null;
184
+ }
185
+ /** Resolves after `ms`, or rejects with the caller's abort reason as soon as it aborts. */
186
+ function pause({ ms, signal }) {
187
+ return new Promise((resolve, reject) => {
188
+ if (signal?.aborted) {
189
+ reject(signal.reason);
190
+ return;
191
+ }
192
+ const aborted = () => {
193
+ clearTimeout(timer);
194
+ reject(signal?.reason);
195
+ };
196
+ const timer = setTimeout(() => {
197
+ signal?.removeEventListener('abort', aborted);
198
+ resolve();
199
+ }, ms);
200
+ signal?.addEventListener('abort', aborted, { once: true });
201
+ });
202
+ }
203
+ function createBrokerTransport({ context, token, getRuntime, fetcher, log }) {
204
+ const send = async (input, signal) => {
104
205
  if (context.exp <= Math.floor(Date.now() / 1000))
105
206
  throw new ArtifactServerError(401, 'request_context_expired', 'Artifact request context expired; begin a new request');
106
207
  const operation = artifactServerRequestSchema.parse(input);
@@ -124,10 +225,32 @@ function createBrokerTransport({ context, token, getRuntime, fetcher }) {
124
225
  signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(60_000)]) : AbortSignal.timeout(60_000),
125
226
  });
126
227
  };
228
+ /**
229
+ * Sends a read again when its connection to Atlas failed before Atlas answered: such failures
230
+ * clear within moments, and a read changes nothing by arriving twice. A write is never sent
231
+ * again, because a reset can come after Atlas acted on it. A response, whatever its status, is
232
+ * Atlas's answer and is returned as is.
233
+ */
234
+ return async (input, { signal, replaySafe = isReadOperation(input) } = {}) => {
235
+ for (let attempt = 1;; attempt++) {
236
+ try {
237
+ return await send(input, signal);
238
+ }
239
+ catch (error) {
240
+ const code = replaySafe && attempt < reconnect.attempts ? transientConnectionCode(error) : null;
241
+ if (code === null)
242
+ throw error;
243
+ log.warn('broker.reconnect', { operationId: input.operation, errorCode: code, count: attempt });
244
+ // Equal jitter, so the reads of one burst don't all reconnect at the same moment.
245
+ const ceiling = reconnect.baseDelayMs * 2 ** (attempt - 1);
246
+ await pause({ ms: ceiling / 2 + (Math.random() * ceiling) / 2, signal });
247
+ }
248
+ }
249
+ };
127
250
  }
128
251
  function createApiClient({ mode, request, json }) {
129
252
  const operation = (path, options = {}) => artifactServerRequestSchema.parse({ operation: 'api', mode, method: options.method ?? 'GET', path, body: options.body });
130
- const fetchResponse = (path, options = {}) => request(operation(path, options), options.signal);
253
+ const fetchResponse = (path, options = {}) => request(operation(path, options), { signal: options.signal });
131
254
  const typedJson = async (path, options = {}) => {
132
255
  // Explicit caller result types match the existing browser SDK; the server validates endpoint schemas.
133
256
  return await json(operation(path, options), options.signal);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What Atlas's error envelope said about a refusal, when the response carried one. Atlas routes
3
+ * answer `{ detail, code? }`: `detail` is the human-readable reason, `code` the route's machine
4
+ * code when it has one (`RUN_TERMINAL`, `EDGE_ID_MISMATCH`, `WORKFLOW_NOT_RUNNING`, …), so an
5
+ * application can branch on a refusal the way a caller of the route itself would.
6
+ */
7
+ export interface ArtifactAtlasRefusal {
8
+ code: string | null;
9
+ detail: string | null;
10
+ }
11
+ export declare class ArtifactServerError extends Error {
12
+ readonly status: number;
13
+ readonly code: string;
14
+ /** Atlas's own account of the refusal; both null when the response had no readable envelope. */
15
+ readonly atlas: ArtifactAtlasRefusal;
16
+ constructor(status: number, code: string, message: string, atlas?: ArtifactAtlasRefusal);
17
+ }
18
+ /**
19
+ * A refused broker call, keeping what Atlas said: the message carries the route's `detail`
20
+ * and `error.atlas` its machine `code`, so a consumer can branch on a Lattice refusal
21
+ * (`EDGE_ID_MISMATCH`, `WORKFLOW_NOT_RUNNING`, …) the way a caller of the route itself would.
22
+ */
23
+ export declare function platformRequestError({ response, operation }: {
24
+ response: Response;
25
+ operation: string;
26
+ }): Promise<ArtifactServerError>;
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ const NO_REFUSAL = { code: null, detail: null };
3
+ export class ArtifactServerError extends Error {
4
+ status;
5
+ code;
6
+ /** Atlas's own account of the refusal; both null when the response had no readable envelope. */
7
+ atlas;
8
+ constructor(status, code, message, atlas = NO_REFUSAL) {
9
+ super(message);
10
+ this.status = status;
11
+ this.code = code;
12
+ this.name = 'ArtifactServerError';
13
+ this.atlas = atlas;
14
+ }
15
+ }
16
+ /** Atlas answers a refusal as `{ detail, code? }`; anything else (a proxy page, an empty body) reads as no envelope. */
17
+ const atlasRefusalSchema = z.object({ detail: z.string().optional(), code: z.string().optional() });
18
+ /**
19
+ * A refused broker call, keeping what Atlas said: the message carries the route's `detail`
20
+ * and `error.atlas` its machine `code`, so a consumer can branch on a Lattice refusal
21
+ * (`EDGE_ID_MISMATCH`, `WORKFLOW_NOT_RUNNING`, …) the way a caller of the route itself would.
22
+ */
23
+ export async function platformRequestError({ response, operation }) {
24
+ const envelope = atlasRefusalSchema.safeParse(await response.json().catch(() => undefined));
25
+ const detail = envelope.success ? envelope.data.detail ?? null : null;
26
+ const code = envelope.success ? envelope.data.code ?? null : null;
27
+ const message = `Atlas rejected the ${operation} request (${response.status})${detail ? `: ${detail}` : ''}`;
28
+ return new ArtifactServerError(response.status, 'platform_request_failed', message, { code, detail });
29
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Exact origin-relative path a Lattice `managed_server` node may POST.
3
+ * No wildcards, query strings, or traversal. Keep this identical to
4
+ * `atlas/lattice/src/define/managed-server-path.ts`.
5
+ */
6
+ export declare function isManagedServerInvokePath(path: string): boolean;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Exact origin-relative path a Lattice `managed_server` node may POST.
3
+ * No wildcards, query strings, or traversal. Keep this identical to
4
+ * `atlas/lattice/src/define/managed-server-path.ts`.
5
+ */
6
+ export function isManagedServerInvokePath(path) {
7
+ if (path.length < 2 || path.length > 512 || !path.startsWith('/') || path.includes('//')) {
8
+ return false;
9
+ }
10
+ if (/[?#\\]/.test(path))
11
+ return false;
12
+ const segments = path.slice(1).split('/');
13
+ return segments.every((segment) => segment !== '.' && segment !== '..' && /^[A-Za-z0-9._~-]+$/.test(segment));
14
+ }
@@ -0,0 +1,290 @@
1
+ import { type ArtifactProcessPrincipalType, type ArtifactProcessRunStatus, type ArtifactProcessTaskView, type ArtifactServerMode, type ArtifactServerRequest } from './broker-contract.js';
2
+ /** A run of a process declared under `server.capabilities.processes.start`. */
3
+ export interface ArtifactProcessStart {
4
+ process: string;
5
+ /** Idempotency key: starting the same run id again returns the existing run. */
6
+ runId?: string;
7
+ title?: string;
8
+ entity?: {
9
+ type: string;
10
+ id: string;
11
+ };
12
+ /** The run's input, readable by the process as `ctx.run.metadata`. */
13
+ metadata?: Record<string, unknown>;
14
+ /**
15
+ * Who works each human node of this run, keyed by node id (`execute`) or by a path into a
16
+ * nested subprocess (`application::qa_review`): the node's advancers when the run reaches it,
17
+ * over the process's own defaults. Users are Auth0 subjects, groups are chat group ids; Atlas
18
+ * refuses principals it has tombstoned. Reassign a parked step later with `assign`.
19
+ */
20
+ assignments?: Record<string, ArtifactProcessPrincipals>;
21
+ }
22
+ export interface ArtifactProcessRun {
23
+ run_id: string;
24
+ process_id: string;
25
+ process_version: string;
26
+ status: 'pending' | 'existing';
27
+ idempotent_replay: boolean;
28
+ /**
29
+ * created: a new run was dispatched · replayed: an undispatched run with this id was dispatched ·
30
+ * existing: the run already ran or is running, so nothing was dispatched · unclaimed: a run with
31
+ * this id exists and was not taken over · queued: the process is at its concurrency limit; the run
32
+ * is `pending` and starts when a slot frees.
33
+ */
34
+ outcome: 'created' | 'replayed' | 'existing' | 'unclaimed' | 'queued';
35
+ }
36
+ /** Which runs of a process declared under `server.capabilities.processes.read` to list. */
37
+ export interface ArtifactProcessRunsQuery {
38
+ process: string;
39
+ status?: ArtifactProcessRunStatus;
40
+ /** Runs about one entity type, or one entity. */
41
+ entity?: {
42
+ type: string;
43
+ id?: string;
44
+ };
45
+ /** Newest first; Lattice caps a page at ARTIFACT_PROCESS_RUNS_LIMIT and defaults to 50. */
46
+ limit?: number;
47
+ /** Soft-archived runs only, instead of the default active ones. */
48
+ archived?: boolean;
49
+ }
50
+ /**
51
+ * The stable fields of a Lattice run row as the read API serves it (Atlas may serve more).
52
+ * `metadata` (the run's input) is redacted on read for every viewer, so an application keeps the
53
+ * data it needs back in its own ORM namespace. Timestamps are ISO strings.
54
+ */
55
+ export interface ArtifactProcessRunRecord {
56
+ id: string;
57
+ processId: string;
58
+ processVersion: string;
59
+ status: ArtifactProcessRunStatus;
60
+ title: string | null;
61
+ entityType: string | null;
62
+ entityId: string | null;
63
+ startNodeId: string | null;
64
+ startedBy: string | null;
65
+ startedAt: string;
66
+ completedAt: string | null;
67
+ archivedAt: string | null;
68
+ error: string | null;
69
+ output: unknown;
70
+ /** Node-produced run state; its access boundary is the run's Viewer relation. */
71
+ state: Record<string, unknown>;
72
+ }
73
+ /** The stable fields of one step attempt of a run. Wait-token credentials and runner plumbing are redacted. */
74
+ export interface ArtifactProcessStepRecord {
75
+ id: string;
76
+ runId: string;
77
+ nodeId: string;
78
+ kind: string;
79
+ attempt: number;
80
+ status: string;
81
+ startedAt: string;
82
+ completedAt: string | null;
83
+ input: unknown;
84
+ output: unknown;
85
+ error: string | null;
86
+ nextEdgeId: string | null;
87
+ waitTokenId: string | null;
88
+ }
89
+ /** The collapsed lifecycle of one node: its latest attempt with finished-branch events applied. */
90
+ export type ArtifactProcessNodeState = 'pending' | 'running' | 'completed' | 'failed' | 'skipped' | 'cancelled' | 'timed_out';
91
+ /** One run as `GET /api/lattice/runs/[runId]` serves it. */
92
+ export interface ArtifactProcessRunSnapshot {
93
+ run: ArtifactProcessRunRecord;
94
+ /** Raw step rows, one per attempt / fan-out branch. */
95
+ steps: ArtifactProcessStepRecord[];
96
+ states: Record<string, ArtifactProcessNodeState>;
97
+ /** Pass to the run's event stream as `?since=` to receive everything after this snapshot. */
98
+ event_cursor: string | null;
99
+ }
100
+ /** Which applied version of a process declared under `processes.read` to describe. */
101
+ export interface ArtifactProcessDescribeQuery {
102
+ process: string;
103
+ /** A run's `processVersion`, to see the template that run was started from. Default: the active version. */
104
+ version?: string;
105
+ }
106
+ export interface ArtifactProcessEdge {
107
+ id: string;
108
+ /** The node this edge routes to. */
109
+ to: string;
110
+ }
111
+ /** A static Artifact Studio (or uri) reference a human node renders as its work surface. */
112
+ export interface ArtifactProcessArtifactRef {
113
+ provider: string;
114
+ artifact_id: string;
115
+ project: string | null;
116
+ role: string | null;
117
+ uri: string | null;
118
+ metadata: Record<string, unknown>;
119
+ }
120
+ export interface ArtifactProcessNodeBase {
121
+ id: string;
122
+ kind: 'automation' | 'agent' | 'human' | 'parallel' | 'subprocess' | 'delay' | 'managed_function' | 'managed_server' | (string & {});
123
+ title: string;
124
+ description: string | null;
125
+ outgoing_edges: ArtifactProcessEdge[];
126
+ }
127
+ /** A human node as the people who work it see it; the engine's own node configuration is not part of this. */
128
+ export interface ArtifactProcessHumanNode extends ArtifactProcessNodeBase {
129
+ kind: 'human';
130
+ /** The manual advance choices: `outgoing_edges` less any dedicated timeout route. */
131
+ advance_edges: ArtifactProcessEdge[];
132
+ /** The author's label for the single completion action, or null for the surface's default. */
133
+ complete_label: string | null;
134
+ /** The author's app-facing metadata (`metadata` on `defineHumanNode`): surface hints, form fields, … */
135
+ metadata: Record<string, unknown>;
136
+ comments_enabled: boolean;
137
+ /** Static refs only; refs a mapper resolves per run are not listed. */
138
+ artifact_refs: ArtifactProcessArtifactRef[];
139
+ }
140
+ export type ArtifactProcessNode = ArtifactProcessNodeBase | ArtifactProcessHumanNode;
141
+ /**
142
+ * One applied version of a process: the template its runs are filled in from. Automation, agent,
143
+ * managed-function, subprocess and parallel internals are the process's own and are never served here.
144
+ */
145
+ export interface ArtifactProcessDefinition {
146
+ id: string;
147
+ name: string;
148
+ description: string | null;
149
+ version: string;
150
+ bundle_hash: string;
151
+ /** Process-level switch: `processes.start` is refused while set. */
152
+ new_runs_disabled: boolean;
153
+ start_node_id: string;
154
+ /** JSON Schema for the `metadata` a start accepts, when the author declared one. */
155
+ input_schema: Record<string, unknown> | null;
156
+ tags: string[];
157
+ /** Every node, flattened: a human node inside a parallel branch is listed beside the top-level nodes. */
158
+ nodes: ArtifactProcessNode[];
159
+ }
160
+ /** Which of the user's human steps to list, across processes declared under `processes.read`. */
161
+ export interface ArtifactProcessTasksQuery {
162
+ /** A subset of the declared processes; default every declared one. */
163
+ processes?: string[];
164
+ /** `active` (default): parked and waiting on the user. `completed`: steps they were assigned that are done. */
165
+ view?: ArtifactProcessTaskView;
166
+ /** Newest first; Lattice caps a page at ARTIFACT_PROCESS_TASKS_LIMIT and defaults to 100. */
167
+ limit?: number;
168
+ }
169
+ /** One human step of the user's, with what the node's template says about working it. Timestamps are ISO strings. */
170
+ export interface ArtifactProcessTask {
171
+ step_id: string;
172
+ run_id: string;
173
+ node_id: string;
174
+ process_id: string;
175
+ process_version: string;
176
+ process_name: string | null;
177
+ title: string;
178
+ description: string | null;
179
+ entity: {
180
+ type: string;
181
+ id: string;
182
+ } | null;
183
+ run_title: string | null;
184
+ advance_edges: ArtifactProcessEdge[];
185
+ complete_label: string | null;
186
+ metadata: Record<string, unknown>;
187
+ /** How the step reached the user: assigned to them, to one of their groups, or reassigned by them. */
188
+ assigned_via: 'user' | 'group' | 'reassigned';
189
+ started_at: string;
190
+ due_at: string | null;
191
+ /** Completed view only. */
192
+ completed_at?: string | null;
193
+ /** Completed view only: `completed`, or `timed_out` for a step that expired instead of being worked. */
194
+ step_status?: string;
195
+ /** Completed view only: the run's status at read time. */
196
+ run_status?: string;
197
+ /** The node's template could not be loaded; identity fields are still exact. */
198
+ template_unavailable?: boolean;
199
+ }
200
+ /** Completing one of the user's parked human steps on a run of a process declared under `processes.act`. */
201
+ export interface ArtifactProcessAdvance {
202
+ runId: string;
203
+ /** Which parked step; required when a parallel block has several human branches parked at once. */
204
+ stepId?: string;
205
+ /** One of the node's `advance_edges`; omitted on a terminal human node. */
206
+ edgeId?: string;
207
+ /** The step's output: the node's form fields, as the process reads them. */
208
+ payload?: Record<string, unknown>;
209
+ }
210
+ export interface ArtifactProcessAdvanceResult {
211
+ step_id: string;
212
+ node_id: string;
213
+ edge_id: string | null;
214
+ /** The step had already been completed with this decision; nothing was re-run. */
215
+ idempotent_replay: boolean;
216
+ }
217
+ /** Users (Auth0 `sub`) and chat groups (group id) to name in an assignment or a share. */
218
+ export interface ArtifactProcessPrincipals {
219
+ users?: string[];
220
+ groups?: string[];
221
+ }
222
+ /** Reassigning a parked human step: the step's advancers become exactly these principals. */
223
+ export interface ArtifactProcessAssign extends ArtifactProcessPrincipals {
224
+ runId: string;
225
+ stepId: string;
226
+ }
227
+ export interface ArtifactProcessAssignResult {
228
+ step_id: string;
229
+ node_id: string;
230
+ assigned: {
231
+ users: string[];
232
+ groups: string[];
233
+ };
234
+ }
235
+ /** The explicit `viewer` grants stored on a run; access derived from the process or a step is not listed. */
236
+ export interface ArtifactProcessRunViewers {
237
+ run_id: string;
238
+ users: string[];
239
+ groups: string[];
240
+ }
241
+ export interface ArtifactProcessRunViewerChange extends ArtifactProcessPrincipals {
242
+ runId: string;
243
+ }
244
+ export interface ArtifactProcessPrincipalSearch {
245
+ /** Empty browses the first `limit` users; 1–2 characters match groups only; 3+ search the directory. */
246
+ q: string;
247
+ /** Default both. */
248
+ types?: ArtifactProcessPrincipalType[];
249
+ /** Users and groups together; Lattice caps at ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, default 20. */
250
+ limit?: number;
251
+ }
252
+ /** A principal the user may name. `id` is the value `assign` and the viewer grants consume. */
253
+ export interface ArtifactProcessPrincipal {
254
+ type: ArtifactProcessPrincipalType;
255
+ id: string;
256
+ name: string;
257
+ email?: string;
258
+ picture?: string;
259
+ /** Groups: active members. */
260
+ memberCount?: number;
261
+ }
262
+ export interface ArtifactServerProcesses {
263
+ /** Start a run of a process declared under `processes.start`; the user's Invoker grant decides. */
264
+ start(input: ArtifactProcessStart): Promise<ArtifactProcessRun>;
265
+ /** List the runs of a process declared under `processes.read` that the user may view. */
266
+ listRuns(query: ArtifactProcessRunsQuery): Promise<ArtifactProcessRunRecord[]>;
267
+ /** Read one run of a process declared under `processes.read`; the user's Viewer relation on the run decides. */
268
+ getRun(runId: string): Promise<ArtifactProcessRunSnapshot>;
269
+ /** Describe a process declared under `processes.read`; the user's Viewer relation on the process decides. */
270
+ describe(query: ArtifactProcessDescribeQuery): Promise<ArtifactProcessDefinition>;
271
+ /** The user's own human steps across processes declared under `processes.read`. */
272
+ listTasks(query?: ArtifactProcessTasksQuery): Promise<ArtifactProcessTask[]>;
273
+ /** Complete a parked human step on a run of a process declared under `processes.act`; the user's own `advancer` grant decides. */
274
+ advance(input: ArtifactProcessAdvance): Promise<ArtifactProcessAdvanceResult>;
275
+ /** Reassign a parked human step; the user's `owner` relation on the run decides. */
276
+ assign(input: ArtifactProcessAssign): Promise<ArtifactProcessAssignResult>;
277
+ /** The explicit viewer grants on a run of a process declared under `processes.act`; owner-gated. */
278
+ listViewers(runId: string): Promise<ArtifactProcessRunViewers>;
279
+ /** Share a run with users and groups; owner-gated, additive, idempotent. */
280
+ grantViewers(input: ArtifactProcessRunViewerChange): Promise<ArtifactProcessRunViewers>;
281
+ /** Take a share back; owner-gated, idempotent. Returns the grants that remain. */
282
+ revokeViewers(input: ArtifactProcessRunViewerChange): Promise<ArtifactProcessRunViewers>;
283
+ /** Users in the user's own tenant and groups they may see, to name in `assign` or a share. */
284
+ searchPrincipals(query: ArtifactProcessPrincipalSearch): Promise<ArtifactProcessPrincipal[]>;
285
+ }
286
+ /** `client.processes`: Lattice through the broker's typed `lattice*` operations. Atlas owns validation and redaction. */
287
+ export declare function createArtifactProcessesClient({ mode, json }: {
288
+ mode: ArtifactServerMode;
289
+ json: (operation: ArtifactServerRequest) => Promise<unknown>;
290
+ }): ArtifactServerProcesses;
@@ -0,0 +1,56 @@
1
+ import { artifactServerRequestSchema, } from './broker-contract.js';
2
+ import { ArtifactServerError } from './errors.js';
3
+ /** `client.processes`: Lattice through the broker's typed `lattice*` operations. Atlas owns validation and redaction. */
4
+ export function createArtifactProcessesClient({ mode, json }) {
5
+ return {
6
+ start: async ({ process, ...run }) => {
7
+ // Atlas returns the started run's identifiers; the shape is the Lattice start response.
8
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice', mode, process, ...run }));
9
+ },
10
+ listRuns: async (query) => {
11
+ // The Lattice list response: `{ runs }`, already filtered to what the user may view.
12
+ const body = await json(artifactServerRequestSchema.parse({ operation: 'lattice_runs', mode, ...query }));
13
+ if (!Array.isArray(body?.runs))
14
+ throw new ArtifactServerError(502, 'malformed_response', 'Atlas returned a malformed run list');
15
+ return body.runs;
16
+ },
17
+ getRun: async (runId) => {
18
+ // The Lattice run-detail response; Atlas owns redaction and node-state derivation.
19
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_run', mode, runId }));
20
+ },
21
+ describe: async (query) => {
22
+ // Atlas projects the applied bundle; engine internals never leave it.
23
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_process', mode, ...query }));
24
+ },
25
+ listTasks: async (query = {}) => {
26
+ // The Lattice inbox response shape: `{ items }`, already the user's own.
27
+ const body = await json(artifactServerRequestSchema.parse({ operation: 'lattice_tasks', mode, ...query }));
28
+ if (!Array.isArray(body?.items))
29
+ throw new ArtifactServerError(502, 'malformed_response', 'Atlas returned a malformed task list');
30
+ return body.items;
31
+ },
32
+ advance: async (input) => {
33
+ // The Lattice advance response; Atlas owns the step resolution, the ACL and the walk.
34
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_advance', mode, ...input }));
35
+ },
36
+ assign: async (input) => {
37
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_assign', mode, ...input }));
38
+ },
39
+ listViewers: async (runId) => {
40
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_viewers', mode, runId }));
41
+ },
42
+ grantViewers: async (input) => {
43
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_viewer_grant', mode, ...input }));
44
+ },
45
+ revokeViewers: async (input) => {
46
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_viewer_revoke', mode, ...input }));
47
+ },
48
+ searchPrincipals: async (query) => {
49
+ // The Lattice picker response shape: `{ principals }`, already scoped to the user's tenant.
50
+ const body = await json(artifactServerRequestSchema.parse({ operation: 'lattice_principals', mode, ...query }));
51
+ if (!Array.isArray(body?.principals))
52
+ throw new ArtifactServerError(502, 'malformed_response', 'Atlas returned a malformed principal list');
53
+ return body.principals;
54
+ },
55
+ };
56
+ }