@sequenceholdings/artifact-studio 0.6.1 → 0.6.3

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 (35) hide show
  1. package/README.md +43 -0
  2. package/VERSION-PIN.md +7 -0
  3. package/dist/manifest-next-ingress.d.ts +213 -0
  4. package/dist/manifest-next-ingress.js +187 -0
  5. package/dist/manifest-next-reach.d.ts +40 -0
  6. package/dist/manifest-next-reach.js +48 -0
  7. package/dist/manifest-next.d.ts +133 -8
  8. package/dist/manifest-next.js +7 -46
  9. package/dist/manifest-static.d.ts +6 -0
  10. package/dist/manifest-static.js +10 -0
  11. package/dist/manifest.d.ts +67 -0
  12. package/dist/manifest.js +7 -0
  13. package/dist/server/api-client.d.ts +1 -0
  14. package/dist/server/api-client.js +2 -1
  15. package/dist/server/broker-contract.d.ts +33 -6
  16. package/dist/server/broker-contract.js +20 -2
  17. package/dist/server/client.js +31 -16
  18. package/dist/server/external-context.js +1 -1
  19. package/dist/server/ingress-control-contract.d.ts +272 -0
  20. package/dist/server/ingress-control-contract.js +196 -0
  21. package/dist/server/ingress-routes.d.ts +76 -0
  22. package/dist/server/ingress-routes.js +150 -0
  23. package/dist/server/internal/verify-any.d.ts +3 -1
  24. package/dist/server/internal/verify-any.js +13 -3
  25. package/dist/server/processes.d.ts +56 -0
  26. package/dist/server/processes.js +13 -0
  27. package/dist/server/request-context.d.ts +8 -4
  28. package/dist/server/request-context.js +8 -6
  29. package/dist/server/runtime-config.d.ts +1 -1
  30. package/dist/templates/nextjs/README.md +9 -0
  31. package/dist/tunnel/protocol.js +2 -4
  32. package/dist/tunnel/reserved-headers.d.ts +7 -0
  33. package/dist/tunnel/reserved-headers.js +20 -0
  34. package/package.json +9 -1
  35. package/templates/nextjs/README.md +9 -0
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { artifactNextAuditCapabilitySchema } from './manifest-next-reach.js';
2
3
  /** Approved major/minor profile. Builds additionally record exact patch versions. */
3
4
  export declare const ARTIFACT_NEXT_RUNTIME: Readonly<{
4
5
  sdk: "sequence";
@@ -20,14 +21,8 @@ export declare const ARTIFACT_NEXT_RESOURCE_DEFAULTS: {
20
21
  max_instances: number;
21
22
  timeout_seconds: number;
22
23
  };
23
- /**
24
- * Where the server's audit events (`client.audit.record`) go: an Atlas audit service id. Every
25
- * application may record under `artifact_studio`; Atlas admits a first-party service, such as
26
- * `application_portal`, only for the projects it registers for it. Server-only: browsers can't audit.
27
- */
28
- export declare const artifactNextAuditCapabilitySchema: z.ZodObject<{
29
- service: z.ZodString;
30
- }, z.core.$strict>;
24
+ export { artifactNextAuditCapabilitySchema };
25
+ export * from './manifest-next-ingress.js';
31
26
  export declare const artifactNextServerCapabilitiesSchema: z.ZodPrefault<z.ZodObject<{
32
27
  audit: z.ZodOptional<z.ZodObject<{
33
28
  service: z.ZodString;
@@ -132,6 +127,71 @@ export declare const artifactNextServerSchema: z.ZodPrefault<z.ZodObject<{
132
127
  lattice: z.ZodOptional<z.ZodObject<{
133
128
  accept: z.ZodDefault<z.ZodArray<z.ZodString>>;
134
129
  }, z.core.$strict>>;
130
+ ingress: z.ZodOptional<z.ZodObject<{
131
+ external: z.ZodArray<z.ZodObject<{
132
+ id: z.ZodString;
133
+ provider: z.ZodString;
134
+ service_account: z.ZodString;
135
+ headers: z.ZodDefault<z.ZodArray<z.ZodString>>;
136
+ capabilities: z.ZodPrefault<z.ZodObject<{
137
+ storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
138
+ viewer: "viewer";
139
+ editor: "editor";
140
+ admin: "admin";
141
+ }>>>;
142
+ data: z.ZodPrefault<z.ZodObject<{
143
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
144
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
145
+ }, z.core.$strict>>;
146
+ api: z.ZodPrefault<z.ZodObject<{
147
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
148
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
149
+ }, z.core.$strict>>;
150
+ functions: z.ZodPrefault<z.ZodObject<{
151
+ invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
152
+ }, z.core.$strict>>;
153
+ audit: z.ZodOptional<z.ZodObject<{
154
+ service: z.ZodString;
155
+ }, z.core.$strict>>;
156
+ processes: z.ZodOptional<z.ZodNever>;
157
+ }, z.core.$strict>>;
158
+ evidence: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
159
+ bind: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
160
+ email: "email";
161
+ subject: "subject";
162
+ authTime: "authTime";
163
+ amr: "amr";
164
+ "client.ip": "client.ip";
165
+ "client.userAgent": "client.userAgent";
166
+ issuer: "issuer";
167
+ }>>>;
168
+ fresh_auth_seconds: z.ZodOptional<z.ZodNumber>;
169
+ }, z.core.$strict>>>;
170
+ routes: z.ZodArray<z.ZodObject<{
171
+ path: z.ZodString;
172
+ methods: z.ZodArray<z.ZodEnum<{
173
+ GET: "GET";
174
+ POST: "POST";
175
+ PUT: "PUT";
176
+ PATCH: "PATCH";
177
+ DELETE: "DELETE";
178
+ }>>;
179
+ fresh_auth_seconds: z.ZodOptional<z.ZodNumber>;
180
+ max_body_bytes: z.ZodDefault<z.ZodNumber>;
181
+ responses: z.ZodDefault<z.ZodArray<z.ZodEnum<{
182
+ "application/json": "application/json";
183
+ "application/problem+json": "application/problem+json";
184
+ "application/pdf": "application/pdf";
185
+ "application/zip": "application/zip";
186
+ "image/png": "image/png";
187
+ "image/jpeg": "image/jpeg";
188
+ "image/webp": "image/webp";
189
+ "text/csv": "text/csv";
190
+ "application/octet-stream": "application/octet-stream";
191
+ }>>>;
192
+ }, z.core.$strict>>;
193
+ }, z.core.$strict>>;
194
+ }, z.core.$strict>>;
135
195
  }, z.core.$strict>>;
136
196
  export declare const artifactStudioNextManifestSchema: z.ZodObject<{
137
197
  ontology: z.ZodOptional<z.ZodObject<{
@@ -251,6 +311,71 @@ export declare const artifactStudioNextManifestSchema: z.ZodObject<{
251
311
  lattice: z.ZodOptional<z.ZodObject<{
252
312
  accept: z.ZodDefault<z.ZodArray<z.ZodString>>;
253
313
  }, z.core.$strict>>;
314
+ ingress: z.ZodOptional<z.ZodObject<{
315
+ external: z.ZodArray<z.ZodObject<{
316
+ id: z.ZodString;
317
+ provider: z.ZodString;
318
+ service_account: z.ZodString;
319
+ headers: z.ZodDefault<z.ZodArray<z.ZodString>>;
320
+ capabilities: z.ZodPrefault<z.ZodObject<{
321
+ storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
322
+ viewer: "viewer";
323
+ editor: "editor";
324
+ admin: "admin";
325
+ }>>>;
326
+ data: z.ZodPrefault<z.ZodObject<{
327
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
328
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
329
+ }, z.core.$strict>>;
330
+ api: z.ZodPrefault<z.ZodObject<{
331
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
332
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
333
+ }, z.core.$strict>>;
334
+ functions: z.ZodPrefault<z.ZodObject<{
335
+ invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
336
+ }, z.core.$strict>>;
337
+ audit: z.ZodOptional<z.ZodObject<{
338
+ service: z.ZodString;
339
+ }, z.core.$strict>>;
340
+ processes: z.ZodOptional<z.ZodNever>;
341
+ }, z.core.$strict>>;
342
+ evidence: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
343
+ bind: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
344
+ email: "email";
345
+ subject: "subject";
346
+ authTime: "authTime";
347
+ amr: "amr";
348
+ "client.ip": "client.ip";
349
+ "client.userAgent": "client.userAgent";
350
+ issuer: "issuer";
351
+ }>>>;
352
+ fresh_auth_seconds: z.ZodOptional<z.ZodNumber>;
353
+ }, z.core.$strict>>>;
354
+ routes: z.ZodArray<z.ZodObject<{
355
+ path: z.ZodString;
356
+ methods: z.ZodArray<z.ZodEnum<{
357
+ GET: "GET";
358
+ POST: "POST";
359
+ PUT: "PUT";
360
+ PATCH: "PATCH";
361
+ DELETE: "DELETE";
362
+ }>>;
363
+ fresh_auth_seconds: z.ZodOptional<z.ZodNumber>;
364
+ max_body_bytes: z.ZodDefault<z.ZodNumber>;
365
+ responses: z.ZodDefault<z.ZodArray<z.ZodEnum<{
366
+ "application/json": "application/json";
367
+ "application/problem+json": "application/problem+json";
368
+ "application/pdf": "application/pdf";
369
+ "application/zip": "application/zip";
370
+ "image/png": "image/png";
371
+ "image/jpeg": "image/jpeg";
372
+ "image/webp": "image/webp";
373
+ "text/csv": "text/csv";
374
+ "application/octet-stream": "application/octet-stream";
375
+ }>>>;
376
+ }, z.core.$strict>>;
377
+ }, z.core.$strict>>;
378
+ }, z.core.$strict>>;
254
379
  }, z.core.$strict>>;
255
380
  targets: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
256
381
  url: z.ZodString;
@@ -1,8 +1,9 @@
1
- import { storageScopesSchema } from '@sequenceholdings/storage';
2
1
  import { ontologyContractSchema } from '@sequenceholdings/orm/canonical';
3
2
  import { z } from 'zod';
4
3
  import { capabilityPinsSchema, customRolesBlockSchema, validateCapabilityPins } from './manifest-custom-roles.js';
5
4
  import { isManagedServerInvokePath } from './server/managed-server-path.js';
5
+ import { artifactNextIngressSchema, validateIngressAgainstServer } from './manifest-next-ingress.js';
6
+ import { artifactNextAuditCapabilitySchema, reachShape, serviceAccountRef } from './manifest-next-reach.js';
6
7
  import { apiCapabilityPathAllowed, artifactPresentationSchema, artifactStudioStaticManifestSchema, sidebarBadgeSchema, } from './manifest-static.js';
7
8
  /** Approved major/minor profile. Builds additionally record exact patch versions. */
8
9
  export const ARTIFACT_NEXT_RUNTIME = Object.freeze({ sdk: 'sequence', node: 24, react: 19, next: '16.3' });
@@ -11,51 +12,8 @@ export const ARTIFACT_NEXT_APPROVED_PACKAGES = Object.freeze({ next: '16.3.4', r
11
12
  export const ARTIFACT_NEXT_RESOURCE_DEFAULTS = {
12
13
  cpu: 1, memory_mb: 1024, concurrency: 20, min_instances: 0, max_instances: 3, timeout_seconds: 60,
13
14
  };
14
- const boundedRef = z.string().min(1).max(255).regex(/^[a-zA-Z0-9][a-zA-Z0-9_./:-]*$/).refine((value) => value.split('/').every((part) => part !== '' && part !== '.' && part !== '..'), 'Resource references cannot contain empty segments or traversal');
15
- const serviceAccountRef = z.string().regex(/^([a-z][a-z0-9-]{1,98}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/);
16
- const apiPath = z.string().min(1).max(512).refine((value) => {
17
- if (!value.startsWith('/api/') || value === '/api/' || value === '/api/*')
18
- return false;
19
- if (/[\\%?#\s\x00-\x1f]/.test(value))
20
- return false;
21
- return value.split('/').slice(1).every((part, index, parts) => part !== '.' && part !== '..' && (part !== '' || index === parts.length - 1));
22
- }, 'API capability paths must be scoped below /api/ without encoded paths, traversal, queries, or fragments');
23
- // ORM references are what a delegated data token covers (orm-data-plane checkDataTokenScope):
24
- // a query needs namespace/table for every table it reads, and a mutation needs its
25
- // namespace/ops/Operation (or table coverage). Any other shape can never match, so it is
26
- // a typo to report here rather than a 403 at request time. The patterns mirror the ORM's
27
- // NAMESPACE_PATTERN, IDENTIFIER_PATTERN and GraphQL operation names; they are kept local so
28
- // browser code that parses manifests does not load the ORM package. platformUsers is the
29
- // ORM's built-in platform-user entity (PLATFORM_USER_TABLE), a logical key outside the
30
- // identifier pattern that queries traversing user relations read.
31
- const ormTableRef = /^[a-z][a-z0-9_]{0,40}\/(?:[a-z][a-z0-9_]{0,62}|platformUsers)$/;
32
- const ormOperationRef = /^[a-z][a-z0-9_]{0,40}\/ops\/[_A-Za-z][_0-9A-Za-z]*$/;
33
- const dataReadRef = boundedRef.refine((ref) => ormTableRef.test(ref), 'ORM read references use namespace/table');
34
- const dataWriteRef = boundedRef.refine((ref) => ormTableRef.test(ref) || ormOperationRef.test(ref), 'ORM write references use namespace/ops/Operation or namespace/table');
35
- const latticeProcessRef = z.string().min(2).max(128).regex(/^[A-Za-z0-9._-]+$/, 'Process references are Lattice process ids');
36
- const reachShape = {
37
- storage: storageScopesSchema.optional(),
38
- data: z.strictObject({ read: z.array(dataReadRef).max(256).default([]), write: z.array(dataWriteRef).max(256).default([]) }).prefault({}),
39
- api: z.strictObject({ read: z.array(apiPath).max(256).default([]), write: z.array(apiPath).max(256).default([]) }).prefault({}),
40
- functions: z.strictObject({ invoke: z.array(boundedRef).max(256).default([]) }).prefault({}),
41
- // `start`: processes the server may start runs of. `read`: processes whose runs, template and
42
- // the user's own steps it may read. `act`: processes whose human steps it may advance and
43
- // reassign, and whose runs' viewers it may manage. All name exact process ids; the user's own
44
- // Invoker / Viewer / Advancer / Owner grants decide what happens.
45
- processes: z.strictObject({
46
- start: z.array(latticeProcessRef).max(256).default([]),
47
- read: z.array(latticeProcessRef).max(256).default([]),
48
- act: z.array(latticeProcessRef).max(256).default([]),
49
- }).prefault({}),
50
- };
51
- /**
52
- * Where the server's audit events (`client.audit.record`) go: an Atlas audit service id. Every
53
- * application may record under `artifact_studio`; Atlas admits a first-party service, such as
54
- * `application_portal`, only for the projects it registers for it. Server-only: browsers can't audit.
55
- */
56
- export const artifactNextAuditCapabilitySchema = z.strictObject({
57
- service: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/, 'Audit services are Atlas audit service ids, such as artifact_studio'),
58
- });
15
+ export { artifactNextAuditCapabilitySchema };
16
+ export * from './manifest-next-ingress.js';
59
17
  export const artifactNextServerCapabilitiesSchema = z.strictObject({
60
18
  ...reachShape,
61
19
  audit: artifactNextAuditCapabilitySchema.optional(),
@@ -118,6 +76,8 @@ export const artifactNextServerSchema = z.strictObject({
118
76
  lattice: z.strictObject({
119
77
  accept: z.array(managedServerInvokePath).max(32).default([]),
120
78
  }).optional(),
79
+ /** Front doors for callers that are not Atlas users. Omitted means none: external callers are refused. */
80
+ ingress: artifactNextIngressSchema.optional(),
121
81
  }).superRefine((server, ctx) => {
122
82
  if (new Set(server.secrets.map((secret) => secret.name)).size !== server.secrets.length) {
123
83
  ctx.addIssue({ code: 'custom', path: ['secrets'], message: 'Each secret environment name must be unique' });
@@ -128,6 +88,7 @@ export const artifactNextServerSchema = z.strictObject({
128
88
  if (server.lattice && new Set(server.lattice.accept).size !== server.lattice.accept.length) {
129
89
  ctx.addIssue({ code: 'custom', path: ['lattice', 'accept'], message: 'Lattice accept paths must be unique' });
130
90
  }
91
+ validateIngressAgainstServer({ server, ctx });
131
92
  }).prefault({});
132
93
  const appRoot = z.string().min(1).max(512).refine((value) => {
133
94
  if (value === '.')
@@ -23,6 +23,12 @@ export declare const artifactOfflineCapabilitySchema: z.ZodObject<{
23
23
  policy_version: z.ZodNumber;
24
24
  }, z.core.$strip>;
25
25
  export type ArtifactOfflineCapability = z.infer<typeof artifactOfflineCapabilitySchema>;
26
+ /**
27
+ * Whether {@link apiCapabilityPathAllowed} treats an entry as a prefix: a trailing `/`, or a
28
+ * trailing `/*` with no other whole-segment `*`. Such an entry admits every path below it,
29
+ * including routes added later. An entry with a mid-path `*` matches whole segments by count.
30
+ */
31
+ export declare function isPrefixApiCapabilityEntry(entry: string): boolean;
26
32
  export declare function apiCapabilityPathAllowed(pathname: string, allowed: readonly string[]): boolean;
27
33
  export declare const artifactStudioStaticManifestSchema: z.ZodObject<{
28
34
  ontology: z.ZodOptional<z.ZodObject<{
@@ -46,6 +46,16 @@ function hasMidPathWildcard(entry) {
46
46
  const parts = entry.split('/');
47
47
  return parts.slice(1, -1).some((part) => part === '*');
48
48
  }
49
+ /**
50
+ * Whether {@link apiCapabilityPathAllowed} treats an entry as a prefix: a trailing `/`, or a
51
+ * trailing `/*` with no other whole-segment `*`. Such an entry admits every path below it,
52
+ * including routes added later. An entry with a mid-path `*` matches whole segments by count.
53
+ */
54
+ export function isPrefixApiCapabilityEntry(entry) {
55
+ if (entry.includes('*') && (!entry.endsWith('/*') || hasMidPathWildcard(entry)))
56
+ return false;
57
+ return entry.endsWith('/*') || entry.endsWith('/');
58
+ }
49
59
  export function apiCapabilityPathAllowed(pathname, allowed) {
50
60
  return allowed.some((entry) => matchesApiCapabilityEntry(pathname, entry));
51
61
  }
@@ -204,6 +204,71 @@ export declare const artifactStudioManifestSchema: z.ZodUnion<readonly [z.ZodObj
204
204
  lattice: z.ZodOptional<z.ZodObject<{
205
205
  accept: z.ZodDefault<z.ZodArray<z.ZodString>>;
206
206
  }, z.core.$strict>>;
207
+ ingress: z.ZodOptional<z.ZodObject<{
208
+ external: z.ZodArray<z.ZodObject<{
209
+ id: z.ZodString;
210
+ provider: z.ZodString;
211
+ service_account: z.ZodString;
212
+ headers: z.ZodDefault<z.ZodArray<z.ZodString>>;
213
+ capabilities: z.ZodPrefault<z.ZodObject<{
214
+ storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
215
+ viewer: "viewer";
216
+ editor: "editor";
217
+ admin: "admin";
218
+ }>>>;
219
+ data: z.ZodPrefault<z.ZodObject<{
220
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
221
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
222
+ }, z.core.$strict>>;
223
+ api: z.ZodPrefault<z.ZodObject<{
224
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
225
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
226
+ }, z.core.$strict>>;
227
+ functions: z.ZodPrefault<z.ZodObject<{
228
+ invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
229
+ }, z.core.$strict>>;
230
+ audit: z.ZodOptional<z.ZodObject<{
231
+ service: z.ZodString;
232
+ }, z.core.$strict>>;
233
+ processes: z.ZodOptional<z.ZodNever>;
234
+ }, z.core.$strict>>;
235
+ evidence: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
236
+ bind: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
237
+ email: "email";
238
+ subject: "subject";
239
+ authTime: "authTime";
240
+ amr: "amr";
241
+ "client.ip": "client.ip";
242
+ "client.userAgent": "client.userAgent";
243
+ issuer: "issuer";
244
+ }>>>;
245
+ fresh_auth_seconds: z.ZodOptional<z.ZodNumber>;
246
+ }, z.core.$strict>>>;
247
+ routes: z.ZodArray<z.ZodObject<{
248
+ path: z.ZodString;
249
+ methods: z.ZodArray<z.ZodEnum<{
250
+ GET: "GET";
251
+ POST: "POST";
252
+ PUT: "PUT";
253
+ PATCH: "PATCH";
254
+ DELETE: "DELETE";
255
+ }>>;
256
+ fresh_auth_seconds: z.ZodOptional<z.ZodNumber>;
257
+ max_body_bytes: z.ZodDefault<z.ZodNumber>;
258
+ responses: z.ZodDefault<z.ZodArray<z.ZodEnum<{
259
+ "application/json": "application/json";
260
+ "application/problem+json": "application/problem+json";
261
+ "application/pdf": "application/pdf";
262
+ "application/zip": "application/zip";
263
+ "image/png": "image/png";
264
+ "image/jpeg": "image/jpeg";
265
+ "image/webp": "image/webp";
266
+ "text/csv": "text/csv";
267
+ "application/octet-stream": "application/octet-stream";
268
+ }>>>;
269
+ }, z.core.$strict>>;
270
+ }, z.core.$strict>>;
271
+ }, z.core.$strict>>;
207
272
  }, z.core.$strict>>;
208
273
  targets: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
209
274
  url: z.ZodString;
@@ -226,3 +291,5 @@ export type ArtifactStudioManifest = ArtifactStudioStaticManifest | ArtifactStud
226
291
  export type ArtifactStudioManifestInput = z.input<typeof artifactStudioManifestSchema>;
227
292
  export declare function parseArtifactStudioManifest(value: unknown): ArtifactStudioManifest;
228
293
  export declare function isNextArtifactManifest(manifest: ArtifactStudioManifest): manifest is ArtifactStudioNextManifest;
294
+ /** Parses an `artifact.bundle.yml` that must be an Artifact Next manifest (schema_version 2). */
295
+ export declare function parseArtifactNextManifest(value: unknown): ArtifactStudioNextManifest;
package/dist/manifest.js CHANGED
@@ -14,3 +14,10 @@ export function parseArtifactStudioManifest(value) {
14
14
  export function isNextArtifactManifest(manifest) {
15
15
  return manifest.artifact.type === 'nextjs';
16
16
  }
17
+ /** Parses an `artifact.bundle.yml` that must be an Artifact Next manifest (schema_version 2). */
18
+ export function parseArtifactNextManifest(value) {
19
+ const manifest = parseArtifactStudioManifest(value);
20
+ if (!isNextArtifactManifest(manifest))
21
+ throw new Error('Expected an Artifact Next manifest (artifact.type nextjs)');
22
+ return manifest;
23
+ }
@@ -8,6 +8,7 @@ export interface ArtifactServerFetchOptions {
8
8
  /** Sends one broker operation; the client's transport (retries, the external-principal gate). */
9
9
  export type ArtifactBrokerRequest = (operation: ArtifactServerRequest, options?: {
10
10
  signal?: AbortSignal;
11
+ stream?: boolean;
11
12
  }) => Promise<Response>;
12
13
  /** `client.api`: Atlas routes reached through the broker as `api` operations. */
13
14
  export declare function createApiClient({ mode, request, json }: {
@@ -22,7 +22,8 @@ export function createApiClient({ mode, request, json }) {
22
22
  })) };
23
23
  },
24
24
  async stream(path, options = {}) {
25
- const response = await fetchResponse(path, options);
25
+ // A stream outlives the broker's answer deadline: only its headers must arrive in time.
26
+ const response = await request(operation(path, options), { signal: options.signal, stream: true });
26
27
  if (!response.ok || !response.body) {
27
28
  throw await stepUpErrorFromResponse(response) ??
28
29
  new ArtifactServerError(response.status, 'stream_unavailable', 'Atlas did not return a readable response');
@@ -3,10 +3,10 @@ export { ORM_JSON_SCALARS_HEADER, ORM_LOSSLESS_JSON_SCALARS, type ArtifactOrmJso
3
3
  export declare const artifactServerMethodSchema: z.ZodEnum<{
4
4
  GET: "GET";
5
5
  POST: "POST";
6
- DELETE: "DELETE";
7
- HEAD: "HEAD";
8
6
  PUT: "PUT";
9
7
  PATCH: "PATCH";
8
+ DELETE: "DELETE";
9
+ HEAD: "HEAD";
10
10
  }>;
11
11
  export declare const artifactServerModeSchema: z.ZodDefault<z.ZodEnum<{
12
12
  service: "service";
@@ -18,6 +18,8 @@ export declare function isArtifactApiPath(path: string): boolean;
18
18
  export declare const ARTIFACT_PROCESS_RUN_STATUSES: readonly ["pending", "running", "completed", "failed", "cancelled", "timed_out"];
19
19
  /** Lattice lists at most this many runs per call. */
20
20
  export declare const ARTIFACT_PROCESS_RUNS_LIMIT = 500;
21
+ /** A run-events read returns at most this many events per call, as the run's stream drains them. */
22
+ export declare const ARTIFACT_PROCESS_RUN_EVENTS_LIMIT = 1000;
21
23
  /** Which of the user's human steps to list: parked and waiting on them, or already done. */
22
24
  export declare const ARTIFACT_PROCESS_TASK_VIEWS: readonly ["active", "completed"];
23
25
  /** Lattice lists at most this many human steps per call, as its own inbox does. */
@@ -45,10 +47,10 @@ export declare const artifactServerRequestSchema: z.ZodDiscriminatedUnion<[z.Zod
45
47
  method: z.ZodEnum<{
46
48
  GET: "GET";
47
49
  POST: "POST";
48
- DELETE: "DELETE";
49
- HEAD: "HEAD";
50
50
  PUT: "PUT";
51
51
  PATCH: "PATCH";
52
+ DELETE: "DELETE";
53
+ HEAD: "HEAD";
52
54
  }>;
53
55
  path: z.ZodString;
54
56
  body: z.ZodOptional<z.ZodUnknown>;
@@ -71,10 +73,10 @@ export declare const artifactServerRequestSchema: z.ZodDiscriminatedUnion<[z.Zod
71
73
  method: z.ZodEnum<{
72
74
  GET: "GET";
73
75
  POST: "POST";
74
- DELETE: "DELETE";
75
- HEAD: "HEAD";
76
76
  PUT: "PUT";
77
77
  PATCH: "PATCH";
78
+ DELETE: "DELETE";
79
+ HEAD: "HEAD";
78
80
  }>;
79
81
  path: z.ZodString;
80
82
  body: z.ZodOptional<z.ZodUnknown>;
@@ -94,6 +96,7 @@ export declare const artifactServerRequestSchema: z.ZodDiscriminatedUnion<[z.Zod
94
96
  }>>;
95
97
  process: z.ZodString;
96
98
  runId: z.ZodOptional<z.ZodString>;
99
+ startNodeId: z.ZodOptional<z.ZodString>;
97
100
  title: z.ZodOptional<z.ZodString>;
98
101
  entity: z.ZodOptional<z.ZodObject<{
99
102
  type: z.ZodString;
@@ -132,6 +135,15 @@ export declare const artifactServerRequestSchema: z.ZodDiscriminatedUnion<[z.Zod
132
135
  user: "user";
133
136
  }>>;
134
137
  runId: z.ZodString;
138
+ }, z.core.$strict>, z.ZodObject<{
139
+ operation: z.ZodLiteral<"lattice_run_events">;
140
+ mode: z.ZodDefault<z.ZodEnum<{
141
+ service: "service";
142
+ user: "user";
143
+ }>>;
144
+ runId: z.ZodString;
145
+ since: z.ZodOptional<z.ZodString>;
146
+ limit: z.ZodOptional<z.ZodNumber>;
135
147
  }, z.core.$strict>, z.ZodObject<{
136
148
  operation: z.ZodLiteral<"lattice_process">;
137
149
  mode: z.ZodDefault<z.ZodEnum<{
@@ -172,6 +184,21 @@ export declare const artifactServerRequestSchema: z.ZodDiscriminatedUnion<[z.Zod
172
184
  }>>;
173
185
  runId: z.ZodString;
174
186
  stepId: z.ZodString;
187
+ }, z.core.$strict>, z.ZodObject<{
188
+ operation: z.ZodLiteral<"lattice_cancel">;
189
+ mode: z.ZodDefault<z.ZodEnum<{
190
+ service: "service";
191
+ user: "user";
192
+ }>>;
193
+ runId: z.ZodString;
194
+ }, z.core.$strict>, z.ZodObject<{
195
+ operation: z.ZodLiteral<"lattice_reset_step">;
196
+ mode: z.ZodDefault<z.ZodEnum<{
197
+ service: "service";
198
+ user: "user";
199
+ }>>;
200
+ runId: z.ZodString;
201
+ stepId: z.ZodString;
175
202
  }, z.core.$strict>, z.ZodObject<{
176
203
  operation: z.ZodLiteral<"lattice_viewers">;
177
204
  mode: z.ZodDefault<z.ZodEnum<{
@@ -31,6 +31,8 @@ const latticeVersion = z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/);
31
31
  export const ARTIFACT_PROCESS_RUN_STATUSES = ['pending', 'running', 'completed', 'failed', 'cancelled', 'timed_out'];
32
32
  /** Lattice lists at most this many runs per call. */
33
33
  export const ARTIFACT_PROCESS_RUNS_LIMIT = 500;
34
+ /** A run-events read returns at most this many events per call, as the run's stream drains them. */
35
+ export const ARTIFACT_PROCESS_RUN_EVENTS_LIMIT = 1000;
34
36
  /** Which of the user's human steps to list: parked and waiting on them, or already done. */
35
37
  export const ARTIFACT_PROCESS_TASK_VIEWS = ['active', 'completed'];
36
38
  /** Lattice lists at most this many human steps per call, as its own inbox does. */
@@ -106,10 +108,12 @@ export const artifactServerRequestSchema = z.discriminatedUnion('operation', [
106
108
  }, 'Expected a Storage API path'), body: z.unknown().optional() }).strict(),
107
109
  z.object({ operation: z.literal('function'), mode, id, params: z.record(z.string(), z.unknown()).optional() }).strict(),
108
110
  // Starts a run of a declared process, as `POST /api/lattice/runs` does: the application names the
109
- // run, passes its input and may seed who works each human node. The start node, version and
110
- // engine stay with the process and its owning routes.
111
+ // run, passes its input, may seed who works each human node and may enter the graph at a node
112
+ // other than the process's start (the route's `start_node_id`). The version and engine stay with
113
+ // the process and its owning routes.
111
114
  z.object({
112
115
  operation: z.literal('lattice'), mode, process: latticeId, runId: latticeId.optional(),
116
+ startNodeId: z.string().min(1).max(255).regex(/^[A-Za-z0-9._-]+$/).optional(),
113
117
  title: z.string().max(500).optional(),
114
118
  entity: z.object({ type: entityRef, id: entityRef }).strict().optional(),
115
119
  metadata: z.record(z.string(), z.unknown()).optional(),
@@ -127,6 +131,14 @@ export const artifactServerRequestSchema = z.discriminatedUnion('operation', [
127
131
  // Reads one run the user may view, as `GET /api/lattice/runs/[runId]` does: the run, its steps, the
128
132
  // collapsed per-node states and the event cursor to resume its stream from.
129
133
  z.object({ operation: z.literal('lattice_run'), mode, runId: latticeId }).strict(),
134
+ // A page of a run's events after a cursor, oldest first, as `GET /api/lattice/runs/[runId]/stream`
135
+ // sends them: `since` is the cursor `lattice_run` returned, or the one the previous page returned.
136
+ // Polling with it sees every event exactly once. The user's Viewer relation on the run decides.
137
+ z.object({
138
+ operation: z.literal('lattice_run_events'), mode, runId: latticeId,
139
+ since: z.string().min(1).max(512).optional(),
140
+ limit: z.number().int().min(1).max(ARTIFACT_PROCESS_RUN_EVENTS_LIMIT).optional(),
141
+ }).strict(),
130
142
  // Describes a declared process — the applied template its runs are filled in from: nodes, edges and
131
143
  // what a person working each human step is shown. The active version, or the one a run pinned.
132
144
  z.object({ operation: z.literal('lattice_process'), mode, process: latticeId, version: latticeVersion.optional() }).strict(),
@@ -150,6 +162,12 @@ export const artifactServerRequestSchema = z.discriminatedUnion('operation', [
150
162
  // The user's run `owner` relation decides; the node's assignee allowlist still applies.
151
163
  z.object({ operation: z.literal('lattice_assign'), mode, runId: latticeId, stepId: latticeId, ...principalSet }).strict()
152
164
  .refine(namesPrincipals, PRINCIPAL_SET_MESSAGE),
165
+ // Cancels a run of a process declared under `processes.act`, with its forked lineage, as
166
+ // `POST /api/lattice/runs/[runId]/cancel` does. The user's Invoker relation on the run decides.
167
+ z.object({ operation: z.literal('lattice_cancel'), mode, runId: latticeId }).strict(),
168
+ // Resets a generating agent step without cancelling its run, as `POST .../steps/[stepId]/reset`
169
+ // does: the step settles and the run re-parks at the node's reset edge. Invoker-gated.
170
+ z.object({ operation: z.literal('lattice_reset_step'), mode, runId: latticeId, stepId: latticeId }).strict(),
153
171
  // The explicit viewer grants on a run, for its owner.
154
172
  z.object({ operation: z.literal('lattice_viewers'), mode, runId: latticeId }).strict(),
155
173
  // Shares a run with users and groups (grants `viewer`), or takes a share back. Owner-gated;
@@ -170,6 +170,8 @@ async function storageApiError(response) {
170
170
  const envelope = storageErrorSchema.safeParse(await response.json().catch(() => undefined));
171
171
  return new StorageApiError(response.status, envelope.success ? envelope.data.error.code : 'request_failed', envelope.success ? envelope.data.error.message : `Storage request failed (${response.status})`);
172
172
  }
173
+ /** How long Atlas has to answer a broker call; for a stream, to send its first byte of headers. */
174
+ const BROKER_RESPONSE_TIMEOUT_MS = 60_000;
173
175
  /**
174
176
  * Broker operations that change nothing in Atlas. An ORM call's caller decides from its manifest
175
177
  * kind; function invokes and process starts can act, and audit delivery resends on its own terms.
@@ -186,7 +188,7 @@ function isReadOperation(operation) {
186
188
  }
187
189
  }
188
190
  function createBrokerTransport({ context, principal, token, getRuntime, fetcher, log }) {
189
- const send = async (input, signal) => {
191
+ const send = async (input, signal, stream = false) => {
190
192
  // The one choke point for every broker operation: an external caller never acts as a user.
191
193
  if (input.mode === 'user' && principal.kind === 'external')
192
194
  throw new ArtifactPrincipalError('external_requires_service');
@@ -199,19 +201,32 @@ function createBrokerTransport({ context, principal, token, getRuntime, fetcher,
199
201
  if (Buffer.byteLength(body, 'utf8') > maxBodyBytes)
200
202
  throw new ArtifactServerError(413, 'request_too_large', 'Server SDK requests are limited to 1 MiB; use Storage uploads for files');
201
203
  const runtime = await getRuntime();
202
- return fetcher(`${runtime.atlasUrl}${ARTIFACT_SERVER_REQUEST_PATH}`, {
203
- method: 'POST',
204
- headers: {
205
- 'Content-Type': 'application/json',
206
- [ARTIFACT_RUNTIME_TOKEN_HEADER]: runtime.sessionToken,
207
- [ARTIFACT_REQUEST_CONTEXT_HEADER]: token,
208
- },
209
- body,
210
- cache: 'no-store',
211
- redirect: 'error',
212
- credentials: 'omit',
213
- signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(60_000)]) : AbortSignal.timeout(60_000),
214
- });
204
+ // The timeout covers the whole call, body included, except for a stream: its timer is cleared
205
+ // once Atlas answers, so a long body isn't cut off mid-read.
206
+ const expiry = new AbortController();
207
+ const timer = setTimeout(() => expiry.abort(new DOMException('Atlas did not answer in time', 'TimeoutError')), BROKER_RESPONSE_TIMEOUT_MS);
208
+ if (!stream)
209
+ timer.unref?.();
210
+ const timeout = expiry.signal;
211
+ try {
212
+ return await fetcher(`${runtime.atlasUrl}${ARTIFACT_SERVER_REQUEST_PATH}`, {
213
+ method: 'POST',
214
+ headers: {
215
+ 'Content-Type': 'application/json',
216
+ [ARTIFACT_RUNTIME_TOKEN_HEADER]: runtime.sessionToken,
217
+ [ARTIFACT_REQUEST_CONTEXT_HEADER]: token,
218
+ },
219
+ body,
220
+ cache: 'no-store',
221
+ redirect: 'error',
222
+ credentials: 'omit',
223
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
224
+ });
225
+ }
226
+ finally {
227
+ if (stream)
228
+ clearTimeout(timer);
229
+ }
215
230
  };
216
231
  /**
217
232
  * Sends a read again when its connection to Atlas failed before Atlas answered: such failures
@@ -219,10 +234,10 @@ function createBrokerTransport({ context, principal, token, getRuntime, fetcher,
219
234
  * again, because a reset can come after Atlas acted on it. A response, whatever its status, is
220
235
  * Atlas's answer and is returned as is.
221
236
  */
222
- return async (input, { signal, replaySafe = isReadOperation(input) } = {}) => {
237
+ return async (input, { signal, replaySafe = isReadOperation(input), stream = false } = {}) => {
223
238
  for (let attempt = 1;; attempt++) {
224
239
  try {
225
- return await send(input, signal);
240
+ return await send(input, signal, stream);
226
241
  }
227
242
  catch (error) {
228
243
  const code = replaySafe && attempt < reconnect.attempts ? transientConnectionCode(error) : null;
@@ -9,7 +9,7 @@ import { verifyAnyArtifactContext } from './internal/verify-any.js';
9
9
  export function verifyArtifactExternalContext({ token, publicKeys, projectId, environment, method, path, accept, now }) {
10
10
  if (!method || !path)
11
11
  throw new ArtifactContextError();
12
- // TODO(C1): canonicalizeIngressPath(path) at the path comparison in internal/verify-any.ts, which the broker shares.
12
+ // verifyAnyArtifactContext canonicalises `path` (canonicalizeIngressPath) before comparing it.
13
13
  const context = verifyAnyArtifactContext({ token, publicKeys, projectId, environment, method, path, now });
14
14
  if (context.v !== 2)
15
15
  throw new ArtifactContextError('external_context_required');