@sequenceholdings/artifact-studio 0.7.1 → 0.8.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.
Files changed (44) hide show
  1. package/README.md +87 -3
  2. package/dist/build.js +4 -2
  3. package/dist/gateway/authorization-cache.js +1 -1
  4. package/dist/gateway/control.d.ts +13 -1
  5. package/dist/gateway/control.js +3 -0
  6. package/dist/gateway/datadog-startup.d.ts +13 -0
  7. package/dist/gateway/datadog-startup.js +51 -0
  8. package/dist/gateway/hosted-proxy.d.ts +2 -0
  9. package/dist/gateway/hosted-proxy.js +9 -11
  10. package/dist/gateway/ingress-handler.js +2 -0
  11. package/dist/gateway/server.d.ts +1 -1
  12. package/dist/gateway/server.js +51 -20
  13. package/dist/gateway/session.d.ts +14 -0
  14. package/dist/gateway/session.js +31 -2
  15. package/dist/gateway-main.js +5 -3
  16. package/dist/manifest-next.d.ts +9 -0
  17. package/dist/manifest-next.js +5 -0
  18. package/dist/manifest.d.ts +3 -0
  19. package/dist/server/agent-responses.d.ts +425 -0
  20. package/dist/server/agent-responses.js +154 -0
  21. package/dist/server/broker-contract.d.ts +17 -0
  22. package/dist/server/broker-contract.js +17 -2
  23. package/dist/server/client.d.ts +5 -0
  24. package/dist/server/client.js +12 -2
  25. package/dist/server/groups.d.ts +32 -0
  26. package/dist/server/groups.js +28 -0
  27. package/dist/server/processes.d.ts +11 -0
  28. package/dist/server/request-authentication.d.ts +42 -0
  29. package/dist/server/request-authentication.js +93 -0
  30. package/dist/server/request-context.d.ts +1 -0
  31. package/dist/server/request-context.js +1 -0
  32. package/dist/server.d.ts +4 -3
  33. package/dist/server.js +2 -2
  34. package/dist/source-boundary.js +13 -5
  35. package/dist/templates/react-vite/package.json +1 -1
  36. package/dist/tunnel/broker.d.ts +14 -1
  37. package/dist/tunnel/broker.js +94 -10
  38. package/dist/tunnel/client.js +6 -0
  39. package/dist/tunnel/redis-types.d.ts +5 -1
  40. package/dist/tunnel/redis.d.ts +12 -0
  41. package/dist/tunnel/redis.js +102 -4
  42. package/dist/tunnel/transport.d.ts +3 -0
  43. package/package.json +2 -2
  44. package/templates/react-vite/package.json +1 -1
@@ -18,6 +18,10 @@ 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
+ /** One page of the caller's own group memberships. */
22
+ export declare const ARTIFACT_GROUP_MEMBERSHIPS_LIMIT = 100;
23
+ /** How far a caller may page their own memberships. The directory is not walked past this. */
24
+ export declare const ARTIFACT_GROUP_MEMBERSHIPS_MAX_PAGE = 100;
21
25
  /** A run page can filter to at most this many entities of one type. */
22
26
  export declare const ARTIFACT_PROCESS_RUN_ENTITIES_LIMIT = 500;
23
27
  /** A run share adds collaborators to at most this many explicitly assigned human nodes. */
@@ -105,6 +109,11 @@ export declare const artifactServerRequestSchema: z.ZodDiscriminatedUnion<[z.Zod
105
109
  }>>;
106
110
  process: z.ZodString;
107
111
  runId: z.ZodOptional<z.ZodString>;
112
+ version: z.ZodOptional<z.ZodString>;
113
+ expectedProcess: z.ZodOptional<z.ZodObject<{
114
+ version: z.ZodString;
115
+ bundleHash: z.ZodString;
116
+ }, z.core.$strict>>;
108
117
  startNodeId: z.ZodOptional<z.ZodString>;
109
118
  title: z.ZodOptional<z.ZodString>;
110
119
  entity: z.ZodOptional<z.ZodObject<{
@@ -298,6 +307,14 @@ export declare const artifactServerRequestSchema: z.ZodDiscriminatedUnion<[z.Zod
298
307
  group: "group";
299
308
  }>>>;
300
309
  limit: z.ZodOptional<z.ZodNumber>;
310
+ }, z.core.$strict>, z.ZodObject<{
311
+ operation: z.ZodLiteral<"groups_memberships">;
312
+ mode: z.ZodDefault<z.ZodEnum<{
313
+ service: "service";
314
+ user: "user";
315
+ }>>;
316
+ page: z.ZodOptional<z.ZodNumber>;
317
+ limit: z.ZodOptional<z.ZodNumber>;
301
318
  }, z.core.$strict>, z.ZodObject<{
302
319
  operation: z.ZodLiteral<"flags">;
303
320
  mode: z.ZodDefault<z.ZodEnum<{
@@ -27,10 +27,16 @@ const id = z.string().min(1).max(255).regex(/^[a-zA-Z0-9_-]+$/);
27
27
  const latticeId = z.string().min(2).max(128).regex(/^[A-Za-z0-9._-]+$/);
28
28
  // A process version is the registry's own label (the bundle hash prefix today); FGA-safe class.
29
29
  const latticeVersion = z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/);
30
+ // An assertion on the resolved immutable target, independent of the optional version selector.
31
+ const expectedProcessIdentity = z.object({ version: latticeVersion, bundleHash: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
30
32
  /** A Lattice run's lifecycle status, as `GET /api/lattice/runs` filters it. */
31
33
  export const ARTIFACT_PROCESS_RUN_STATUSES = ['pending', 'running', 'completed', 'failed', 'cancelled', 'timed_out'];
32
34
  /** Lattice lists at most this many runs per call. */
33
35
  export const ARTIFACT_PROCESS_RUNS_LIMIT = 500;
36
+ /** One page of the caller's own group memberships. */
37
+ export const ARTIFACT_GROUP_MEMBERSHIPS_LIMIT = 100;
38
+ /** How far a caller may page their own memberships. The directory is not walked past this. */
39
+ export const ARTIFACT_GROUP_MEMBERSHIPS_MAX_PAGE = 100;
34
40
  /** A run page can filter to at most this many entities of one type. */
35
41
  export const ARTIFACT_PROCESS_RUN_ENTITIES_LIMIT = 500;
36
42
  /** A run share adds collaborators to at most this many explicitly assigned human nodes. */
@@ -124,10 +130,12 @@ export const artifactServerRequestSchema = z.discriminatedUnion('operation', [
124
130
  z.object({ operation: z.literal('function'), mode, id, params: z.record(z.string(), z.unknown()).optional() }).strict(),
125
131
  // Starts a run of a declared process, as `POST /api/lattice/runs` does: the application names the
126
132
  // run, passes its input, may seed who works each human node and may enter the graph at a node
127
- // other than the process's start (the route's `start_node_id`). The version and engine stay with
128
- // the process and its owning routes.
133
+ // other than the process's start (the route's `start_node_id`). A registered version may be
134
+ // selected explicitly; omitting it follows the active version. The engine stays server-owned.
129
135
  z.object({
130
136
  operation: z.literal('lattice'), mode, process: latticeId, runId: latticeId.optional(),
137
+ version: latticeVersion.optional(),
138
+ expectedProcess: expectedProcessIdentity.optional(),
131
139
  startNodeId: z.string().min(1).max(255).regex(/^[A-Za-z0-9._-]+$/).optional(),
132
140
  title: z.string().max(500).optional(),
133
141
  entity: z.object({ type: entityRef, id: entityRef }).strict().optional(),
@@ -225,6 +233,13 @@ export const artifactServerRequestSchema = z.discriminatedUnion('operation', [
225
233
  types: z.array(z.enum(ARTIFACT_PROCESS_PRINCIPAL_TYPES)).min(1).max(2).optional(),
226
234
  limit: z.number().int().min(1).max(ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT).optional(),
227
235
  }).strict(),
236
+ // The signed-in user's own group memberships. No search, no admin directory, no group-id allowlist:
237
+ // the directory returns the groups this user is in. Service mode is refused by the handler.
238
+ z.object({
239
+ operation: z.literal('groups_memberships'), mode,
240
+ page: z.number().int().min(1).max(ARTIFACT_GROUP_MEMBERSHIPS_MAX_PAGE).optional(),
241
+ limit: z.number().int().min(1).max(ARTIFACT_GROUP_MEMBERSHIPS_LIMIT).optional(),
242
+ }).strict(),
228
243
  // Evaluates feature flags as Atlas evaluates its own: for the user, or with no user in service mode.
229
244
  // A read: it needs no manifest declaration, as the browser bridge's flags.isEnabled needs none.
230
245
  z.object({ operation: z.literal('flags'), mode, names: z.array(flagName).min(1).max(ARTIFACT_FLAGS_PER_REQUEST) }).strict(),
@@ -1,15 +1,18 @@
1
1
  import { storageClient } from '@sequenceholdings/storage';
2
2
  import { type OperationExecutor } from '@sequenceholdings/orm/client';
3
3
  import { type ArtifactAuditAction, type ArtifactServerMode } from './broker-contract.js';
4
+ import { type ArtifactServerGroups } from './groups.js';
4
5
  import { type ArtifactServerProcesses } from './processes.js';
5
6
  import { type ArtifactOrmJsonScalars } from '../orm-json-scalars.js';
6
7
  import { createArtifactLogger, type ArtifactLogRecord } from './logger.js';
7
8
  import { type ArtifactRequestContext, type RequestContextV2External } from './request-context.js';
8
9
  import { type ArtifactExternalAccept } from './external-context.js';
9
10
  import { createApiClient } from './api-client.js';
11
+ import { type ArtifactServerAgents } from './agent-responses.js';
10
12
  import { type ArtifactPrincipal } from './principal.js';
11
13
  import { type ArtifactRuntimeConfig, type ArtifactRequestHeaders } from './runtime-config.js';
12
14
  export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './errors.js';
15
+ export type * from './groups.js';
13
16
  export type * from './processes.js';
14
17
  export type { ArtifactServerFetchOptions } from './api-client.js';
15
18
  /**
@@ -77,11 +80,13 @@ export interface ArtifactServerClient<TContext extends ArtifactServerContext = A
77
80
  mode: ArtifactServerMode;
78
81
  log: ReturnType<typeof createArtifactLogger>;
79
82
  api: ReturnType<typeof createApiClient>;
83
+ agents: ArtifactServerAgents;
80
84
  storage: ReturnType<typeof storageClient>;
81
85
  functions: {
82
86
  invoke<TOutput = unknown>(id: string, params?: Record<string, unknown>): Promise<TOutput>;
83
87
  };
84
88
  processes: ArtifactServerProcesses;
89
+ groups: ArtifactServerGroups;
85
90
  flags: ArtifactServerFlags;
86
91
  audit: ArtifactServerAudit;
87
92
  orm(input: ArtifactOrmOptions): OperationExecutor;
@@ -3,6 +3,7 @@ import { StorageApiError, storageClient, storageErrorSchema } from '@sequencehol
3
3
  import { OrmOperationError } from '@sequenceholdings/orm/client';
4
4
  import { z } from 'zod';
5
5
  import { artifactOrmIdempotencyKeySchema, artifactServerRequestSchema } from './broker-contract.js';
6
+ import { createArtifactGroupsClient } from './groups.js';
6
7
  import { createArtifactProcessesClient } from './processes.js';
7
8
  import { assertJsonScalarsAcknowledged, ORM_JSON_SCALARS_HEADER } from '../orm-json-scalars.js';
8
9
  import { createArtifactLogger } from './logger.js';
@@ -10,6 +11,7 @@ import { ARTIFACT_REQUEST_CONTEXT_HEADER, ARTIFACT_RUNTIME_TOKEN_HEADER, ARTIFAC
10
11
  import { verifyArtifactExternalContext } from './external-context.js';
11
12
  import { isExternalContextToken } from './internal/signed-token.js';
12
13
  import { createApiClient } from './api-client.js';
14
+ import { createArtifactAgentsClient } from './agent-responses.js';
13
15
  import { pause, transientConnectionCode } from './connection-retry.js';
14
16
  import { principalFromContext } from './principal.js';
15
17
  import { artifactRuntimeConfigSchema, readArtifactRuntimeConfig } from './runtime-config.js';
@@ -51,7 +53,12 @@ export async function createArtifactServerClient({ headers, runtime, fetch: fetc
51
53
  throw new ArtifactServerError(401, 'runtime_changed', 'Artifact runtime disconnected or changed; reload the preview');
52
54
  return current;
53
55
  };
54
- return buildClient({ context, principal: principalFromContext(context), token, getRuntime, fetcher, mode: 'user', logSink });
56
+ // Opaque proof of the gateway's actual browser authentication. Atlas verifies it
57
+ // against this exact signed context; it grants nothing when copied to another request.
58
+ const authenticationToken = headers.get('x-sequence-artifact-authentication') ?? undefined;
59
+ if (authenticationToken !== undefined && authenticationToken.length > 4096)
60
+ throw new ArtifactServerError(401, 'invalid_request_context', 'Artifact gateway authentication proof exceeds its header limit');
61
+ return buildClient({ context, principal: principalFromContext(context), token, authenticationToken, getRuntime, fetcher, mode: 'user', logSink });
55
62
  }
56
63
  function buildClient(args) {
57
64
  const { context, principal, mode, logSink } = args;
@@ -88,6 +95,7 @@ function buildClient(args) {
88
95
  mode,
89
96
  log,
90
97
  api: createApiClient({ mode, request, json }),
98
+ agents: createArtifactAgentsClient({ mode, json }),
91
99
  storage: storageClient({ request: (method, path, body) => storageJson(artifactServerRequestSchema.parse({ operation: 'storage', mode, method, path, body })) }),
92
100
  functions: {
93
101
  invoke: async (id, params) => {
@@ -96,6 +104,7 @@ function buildClient(args) {
96
104
  },
97
105
  },
98
106
  processes: createArtifactProcessesClient({ mode, json }),
107
+ groups: createArtifactGroupsClient({ mode, json }),
99
108
  flags: {
100
109
  evaluate: evaluateFlags,
101
110
  isEnabled: async (flagName) => (await evaluateFlags([flagName]))[flagName] === true,
@@ -211,7 +220,7 @@ function isReadOperation(operation) {
211
220
  return false;
212
221
  }
213
222
  }
214
- function createBrokerTransport({ context, principal, token, getRuntime, fetcher, log }) {
223
+ function createBrokerTransport({ context, principal, token, authenticationToken, getRuntime, fetcher, log }) {
215
224
  const send = async (input, signal, stream = false) => {
216
225
  // The one choke point for every broker operation: an external caller never acts as a user.
217
226
  if (input.mode === 'user' && principal.kind === 'external')
@@ -239,6 +248,7 @@ function createBrokerTransport({ context, principal, token, getRuntime, fetcher,
239
248
  'Content-Type': 'application/json',
240
249
  [ARTIFACT_RUNTIME_TOKEN_HEADER]: runtime.sessionToken,
241
250
  [ARTIFACT_REQUEST_CONTEXT_HEADER]: token,
251
+ ...(authenticationToken !== undefined ? { 'x-sequence-artifact-authentication': authenticationToken } : {}),
242
252
  },
243
253
  body,
244
254
  cache: 'no-store',
@@ -0,0 +1,32 @@
1
+ import { type ArtifactServerMode, type ArtifactServerRequest } from './broker-contract.js';
2
+ /** One group the signed-in user belongs to. `userRole` is `admin` when they administer it. */
3
+ export interface ArtifactGroupMembership {
4
+ id: string;
5
+ userRole: 'admin' | 'member';
6
+ memberCount: number;
7
+ }
8
+ export interface ArtifactGroupMembershipsQuery {
9
+ page?: number;
10
+ limit?: number;
11
+ }
12
+ export interface ArtifactGroupMembershipsPage {
13
+ groups: ArtifactGroupMembership[];
14
+ pagination: {
15
+ page: number;
16
+ limit: number;
17
+ totalCount: number;
18
+ totalPages: number;
19
+ };
20
+ }
21
+ export interface ArtifactServerGroups {
22
+ /**
23
+ * The signed-in user's group memberships. Requires `server.capabilities.groups.read`.
24
+ * Atlas decides which groups they are in; this call cannot name groups, search the directory,
25
+ * or read another user's memberships.
26
+ */
27
+ list(query?: ArtifactGroupMembershipsQuery): Promise<ArtifactGroupMembershipsPage>;
28
+ }
29
+ export declare function createArtifactGroupsClient({ mode, json }: {
30
+ mode: ArtifactServerMode;
31
+ json: (operation: ArtifactServerRequest) => Promise<unknown>;
32
+ }): ArtifactServerGroups;
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import { ARTIFACT_GROUP_MEMBERSHIPS_LIMIT, artifactServerRequestSchema, } from './broker-contract.js';
3
+ import { ArtifactServerError } from './errors.js';
4
+ const membershipsPageSchema = z.object({
5
+ groups: z.array(z.object({
6
+ id: z.string().min(1).max(255),
7
+ userRole: z.enum(['admin', 'member']),
8
+ memberCount: z.number().int().nonnegative(),
9
+ }).strict()),
10
+ pagination: z.object({
11
+ page: z.number().int().min(1),
12
+ limit: z.number().int().min(1).max(ARTIFACT_GROUP_MEMBERSHIPS_LIMIT),
13
+ totalCount: z.number().int().nonnegative(),
14
+ totalPages: z.number().int().nonnegative(),
15
+ }).strict(),
16
+ }).strict();
17
+ export function createArtifactGroupsClient({ mode, json }) {
18
+ return {
19
+ async list(query = {}) {
20
+ const parsed = membershipsPageSchema.safeParse(await json(artifactServerRequestSchema.parse({
21
+ operation: 'groups_memberships', mode, ...query,
22
+ })));
23
+ if (!parsed.success)
24
+ throw new ArtifactServerError(502, 'invalid_response', 'Atlas returned a malformed groups response');
25
+ return parsed.data;
26
+ },
27
+ };
28
+ }
@@ -7,6 +7,17 @@ export type { ArtifactProcessRunSharingAuthoritiesQuery, ArtifactProcessRunShari
7
7
  /** A run of a process declared under `server.capabilities.processes.start`. */
8
8
  export interface ArtifactProcessStart {
9
9
  process: string;
10
+ /** Select a registered immutable version. Omit to start the process's current active version. */
11
+ version?: string;
12
+ /**
13
+ * Require the resolved immutable target to match this version and bundle hash before Atlas
14
+ * creates the run. Drift returns 409 PROCESS_IDENTITY_MISMATCH; this assertion does not select
15
+ * a version. Use `version` separately to start a captured target after active-version promotion.
16
+ */
17
+ expectedProcess?: {
18
+ version: string;
19
+ bundleHash: string;
20
+ };
10
21
  /** Idempotency key: starting the same run id again returns the existing run. */
11
22
  runId?: string;
12
23
  /**
@@ -0,0 +1,42 @@
1
+ import { type ArtifactGatewayPublicKeys, type ArtifactRequestContext } from './request-context.js';
2
+ export declare const ARTIFACT_REQUEST_AUTHENTICATION_HEADER = "x-sequence-artifact-authentication";
3
+ export declare const ARTIFACT_REQUEST_AUTHENTICATION_MAX_LENGTH = 4096;
4
+ export declare class ArtifactRequestHeaderError extends Error {
5
+ constructor();
6
+ }
7
+ /** Internal forwarding validation; the signed proof is verified by the receiving broker. */
8
+ export declare function validatedArtifactAuthenticationHeader({ token, contextToken, external }: {
9
+ token: unknown;
10
+ contextToken: unknown;
11
+ external?: boolean;
12
+ }): string | undefined;
13
+ /** Internal transport guard: trusted appended headers share the same 32 KiB transport budget. */
14
+ export declare function assertArtifactRequestHeaderBudget({ headers }: {
15
+ headers: Readonly<Record<string, string | string[]>>;
16
+ }): void;
17
+ /** Add only the gateway-controlled value after all browser-owned private headers were stripped. */
18
+ export declare function appendArtifactAuthenticationHeader({ headers, token, contextToken }: {
19
+ headers: Record<string, string | string[]>;
20
+ token: unknown;
21
+ contextToken: unknown;
22
+ }): void;
23
+ /** Cloud transport identity headers may come only from the platform's own credential provider. */
24
+ export declare function stripArtifactCloudHeaders({ headers }: {
25
+ headers: Record<string, string | string[]>;
26
+ }): void;
27
+ /**
28
+ * Verifies actual authentication separately from actor attribution. An absent proof keeps legacy
29
+ * contexts usable, but supplies no human authority. A present invalid proof always fails closed.
30
+ * The exact signed context bytes bind even bridge, deployment and service-account fields that
31
+ * are not repeated in this small proof. No application-authored context can supply this evidence.
32
+ */
33
+ export declare function verifyArtifactRequestAuthentication({ token, requestContextToken, context, publicKeys, now }: {
34
+ token: string | null | undefined;
35
+ requestContextToken: string;
36
+ context: ArtifactRequestContext;
37
+ publicKeys: ArtifactGatewayPublicKeys;
38
+ now?: number;
39
+ }): Readonly<{
40
+ authenticationMethod: 'auth0_user';
41
+ sub: string;
42
+ }> | undefined;
@@ -0,0 +1,93 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { validateHeaderValue } from 'node:http';
3
+ import { z } from 'zod';
4
+ import { ArtifactContextError, artifactRequestContextSchema, verifyArtifactRequestContext } from './request-context.js';
5
+ import { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX, verifySignedContext } from './internal/signed-token.js';
6
+ export const ARTIFACT_REQUEST_AUTHENTICATION_HEADER = 'x-sequence-artifact-authentication';
7
+ export const ARTIFACT_REQUEST_AUTHENTICATION_MAX_LENGTH = 4096;
8
+ export class ArtifactRequestHeaderError extends Error {
9
+ constructor() { super('Artifact request headers exceed the transport limit'); this.name = 'ArtifactRequestHeaderError'; }
10
+ }
11
+ /** Internal forwarding validation; the signed proof is verified by the receiving broker. */
12
+ export function validatedArtifactAuthenticationHeader({ token, contextToken, external = false }) {
13
+ if (token === undefined)
14
+ return undefined;
15
+ if (typeof token !== 'string' || !token || token.length > ARTIFACT_REQUEST_AUTHENTICATION_MAX_LENGTH ||
16
+ typeof contextToken !== 'string' || !contextToken || external)
17
+ throw new Error('Invalid Artifact request authentication');
18
+ validateHeaderValue(ARTIFACT_REQUEST_AUTHENTICATION_HEADER, token);
19
+ return token;
20
+ }
21
+ /** Internal transport guard: trusted appended headers share the same 32 KiB transport budget. */
22
+ export function assertArtifactRequestHeaderBudget({ headers }) {
23
+ let bytes = 0;
24
+ const entries = Object.entries(headers);
25
+ if (entries.length > 128)
26
+ throw new ArtifactRequestHeaderError();
27
+ for (const [name, value] of entries) {
28
+ // Node may emit array values as repeated lines. Counting each name/CRLF is conservative
29
+ // even for cookie values, which Node joins instead of repeating.
30
+ for (const item of Array.isArray(value) ? value : [value]) {
31
+ bytes += Buffer.byteLength(name) + Buffer.byteLength(item) + 4;
32
+ if (bytes > 32 * 1024)
33
+ throw new ArtifactRequestHeaderError();
34
+ }
35
+ }
36
+ }
37
+ /** Add only the gateway-controlled value after all browser-owned private headers were stripped. */
38
+ export function appendArtifactAuthenticationHeader({ headers, token, contextToken }) {
39
+ const authentication = validatedArtifactAuthenticationHeader({ token, contextToken });
40
+ if (authentication !== undefined)
41
+ headers[ARTIFACT_REQUEST_AUTHENTICATION_HEADER] = authentication;
42
+ assertArtifactRequestHeaderBudget({ headers });
43
+ }
44
+ /** Cloud transport identity headers may come only from the platform's own credential provider. */
45
+ export function stripArtifactCloudHeaders({ headers }) {
46
+ for (const name of Object.keys(headers)) {
47
+ if (name === 'x-serverless-authorization' || name.startsWith('x-goog-'))
48
+ delete headers[name];
49
+ }
50
+ }
51
+ /** Internal signed format. Authentication is deliberately absent from the public v1 context. */
52
+ const authenticationProofSchema = z.object({
53
+ purpose: z.literal('artifact-request-authentication'), v: z.literal(1),
54
+ iss: z.literal('atlas-artifact-gateway'), aud: z.string().min(1).max(300),
55
+ authenticationMethod: z.literal('auth0_user'), sub: z.string().min(1).max(255),
56
+ contextJti: z.uuid(), contextSha256: z.string().regex(/^[a-f0-9]{64}$/),
57
+ projectId: z.string().min(1).max(255), environment: z.string().min(1).max(255),
58
+ method: z.enum(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']), pathSha256: z.string().regex(/^[a-f0-9]{64}$/),
59
+ iat: z.number().int().nonnegative(), exp: z.number().int().positive(),
60
+ }).strict();
61
+ /**
62
+ * Verifies actual authentication separately from actor attribution. An absent proof keeps legacy
63
+ * contexts usable, but supplies no human authority. A present invalid proof always fails closed.
64
+ * The exact signed context bytes bind even bridge, deployment and service-account fields that
65
+ * are not repeated in this small proof. No application-authored context can supply this evidence.
66
+ */
67
+ export function verifyArtifactRequestAuthentication({ token, requestContextToken, context, publicKeys, now = Math.floor(Date.now() / 1000) }) {
68
+ if (token === null || token === undefined)
69
+ return undefined;
70
+ try {
71
+ if (!token || token.length > ARTIFACT_REQUEST_AUTHENTICATION_MAX_LENGTH)
72
+ throw new ArtifactContextError();
73
+ const signedContext = verifyArtifactRequestContext({ token: requestContextToken, publicKeys,
74
+ projectId: context.projectId, environment: context.environment, method: context.method, path: context.path, now });
75
+ if (JSON.stringify(signedContext) !== JSON.stringify(artifactRequestContextSchema.parse(context)))
76
+ throw new ArtifactContextError();
77
+ const contextSignature = verifySignedContext({ token: requestContextToken, publicKeys });
78
+ const verified = verifySignedContext({ token, publicKeys });
79
+ if (verified.kid.startsWith(ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX) || verified.kid !== contextSignature.kid)
80
+ throw new ArtifactContextError();
81
+ const proof = authenticationProofSchema.parse(verified.payload);
82
+ if (proof.contextSha256 !== createHash('sha256').update(requestContextToken).digest('hex') ||
83
+ proof.contextJti !== signedContext.jti || proof.sub !== signedContext.sub || proof.aud !== signedContext.aud ||
84
+ proof.projectId !== signedContext.projectId || proof.environment !== signedContext.environment ||
85
+ proof.method !== signedContext.method || proof.pathSha256 !== createHash('sha256').update(signedContext.path).digest('hex') ||
86
+ proof.iat !== signedContext.iat || proof.exp !== signedContext.exp)
87
+ throw new ArtifactContextError();
88
+ return Object.freeze({ authenticationMethod: proof.authenticationMethod, sub: proof.sub });
89
+ }
90
+ catch {
91
+ throw new ArtifactContextError();
92
+ }
93
+ }
@@ -195,3 +195,4 @@ export declare function verifyArtifactRequestContext({ token, publicKeys, projec
195
195
  export declare function getArtifactBrowserBootstrap(context: ArtifactRequestContext | RequestContextV2External): NextArtifactBridgeBootstrap & {
196
196
  initialContext: NextArtifactInitialContext;
197
197
  };
198
+ export { ARTIFACT_REQUEST_AUTHENTICATION_HEADER, verifyArtifactRequestAuthentication } from './request-authentication.js';
@@ -193,3 +193,4 @@ export function getArtifactBrowserBootstrap(context) {
193
193
  },
194
194
  };
195
195
  }
196
+ export { ARTIFACT_REQUEST_AUTHENTICATION_HEADER, verifyArtifactRequestAuthentication } from './request-authentication.js';
package/dist/server.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import 'server-only';
2
2
  export { createArtifactServerClient } from './server/client.js';
3
+ export type { ArtifactServerAgents, ArtifactCompletedAgentRun, ArtifactCompletedAgentSourceCapture, ArtifactCompletedAgentConversationCapture, ArtifactAgentConversationSourceCapture, ArtifactExecutionIdentity, ArtifactCreateAgentJob, ArtifactCreateWorkflowJob, ArtifactCreatedAgentJob, ArtifactCreatedWorkflowJob, ArtifactCompletedWorkflowSourceCapture } from './server/agent-responses.js';
3
4
  export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './server/errors.js';
4
5
  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, ArtifactProcessRunSummary, ArtifactProcessRunsPage, ArtifactProcessRunsPageQuery, ArtifactProcessRunsQuery, ArtifactProcessRunViewerChange, ArtifactProcessRunViewers, ArtifactProcessStart, ArtifactProcessStepRecord, ArtifactProcessShareRun, ArtifactProcessShareRunResult, ArtifactProcessRunSharingAuthoritiesQuery, ArtifactProcessRunSharingAuthoritiesPage, ArtifactProcessRunSharingAuthority, ArtifactProcessRunSharingDelegate, ArtifactProcessRevokeRunSharingAuthority, ArtifactProcessRevokeRunSharingAuthorityResult, 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_RUN_ENTITIES_LIMIT, ARTIFACT_PROCESS_SHARE_NODES_LIMIT, ARTIFACT_PROCESS_SHARING_AUTHORITIES_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
6
+ export type { ArtifactAuditEvent, ArtifactOrmOperations, ArtifactOrmOptions, ArtifactProcessAdvance, ArtifactProcessAdvanceResult, ArtifactProcessArtifactRef, ArtifactProcessAssign, ArtifactProcessAssignResult, ArtifactProcessDefinition, ArtifactProcessDescribeQuery, ArtifactProcessEdge, ArtifactProcessHumanNode, ArtifactProcessNode, ArtifactProcessNodeBase, ArtifactProcessNodeState, ArtifactProcessPrincipal, ArtifactProcessPrincipalSearch, ArtifactProcessPrincipals, ArtifactProcessRun, ArtifactProcessRunRecord, ArtifactProcessRunSnapshot, ArtifactProcessRunSummary, ArtifactProcessRunsPage, ArtifactProcessRunsPageQuery, ArtifactProcessRunsQuery, ArtifactProcessRunViewerChange, ArtifactProcessRunViewers, ArtifactProcessStart, ArtifactProcessStepRecord, ArtifactProcessShareRun, ArtifactProcessShareRunResult, ArtifactProcessRunSharingAuthoritiesQuery, ArtifactProcessRunSharingAuthoritiesPage, ArtifactProcessRunSharingAuthority, ArtifactProcessRunSharingDelegate, ArtifactProcessRevokeRunSharingAuthority, ArtifactProcessRevokeRunSharingAuthorityResult, ArtifactProcessTask, ArtifactProcessTasksQuery, ArtifactServerAudit, ArtifactServerClient, ArtifactServerClientOptions, ArtifactServerContext, ArtifactServerFetchOptions, ArtifactServerFlags, ArtifactServerGroups, ArtifactServerProcesses, ArtifactGroupMembership, ArtifactGroupMembershipsPage, ArtifactGroupMembershipsQuery, } from './server/client.js';
7
+ export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_GROUP_MEMBERSHIPS_LIMIT, ARTIFACT_GROUP_MEMBERSHIPS_MAX_PAGE, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_RUN_ENTITIES_LIMIT, ARTIFACT_PROCESS_SHARE_NODES_LIMIT, ARTIFACT_PROCESS_SHARING_AUTHORITIES_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
7
8
  export type { ArtifactAuditAction, ArtifactProcessPrincipalType, ArtifactProcessRunStatus, ArtifactProcessTaskView, } from './server/broker-contract.js';
8
9
  export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
9
10
  export type { ArtifactRuntimeConfig, ArtifactRequestHeaders } from './server/runtime-config.js';
10
- export { ArtifactContextError, getArtifactBrowserBootstrap } from './server/request-context.js';
11
+ export { ArtifactContextError, getArtifactBrowserBootstrap, ARTIFACT_REQUEST_AUTHENTICATION_HEADER, verifyArtifactRequestAuthentication } from './server/request-context.js';
11
12
  export type { ArtifactContextErrorReason, ArtifactRequestContext, RequestContextV2External } from './server/request-context.js';
12
13
  export { verifyArtifactExternalContext } from './server/external-context.js';
13
14
  export type { ArtifactExternalAccept } from './server/external-context.js';
package/dist/server.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import 'server-only';
2
2
  export { createArtifactServerClient } from './server/client.js';
3
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_RUN_ENTITIES_LIMIT, ARTIFACT_PROCESS_SHARE_NODES_LIMIT, ARTIFACT_PROCESS_SHARING_AUTHORITIES_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
4
+ export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_GROUP_MEMBERSHIPS_LIMIT, ARTIFACT_GROUP_MEMBERSHIPS_MAX_PAGE, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_RUN_ENTITIES_LIMIT, ARTIFACT_PROCESS_SHARE_NODES_LIMIT, ARTIFACT_PROCESS_SHARING_AUTHORITIES_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
5
5
  export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
6
- export { ArtifactContextError, getArtifactBrowserBootstrap } from './server/request-context.js';
6
+ export { ArtifactContextError, getArtifactBrowserBootstrap, ARTIFACT_REQUEST_AUTHENTICATION_HEADER, verifyArtifactRequestAuthentication } from './server/request-context.js';
7
7
  export { verifyArtifactExternalContext } from './server/external-context.js';
8
8
  export { externalEvidence } from './server/principal.js';
@@ -1,3 +1,4 @@
1
+ import { realpathSync } from 'node:fs';
1
2
  import { isAbsolute, relative, resolve, sep } from 'node:path';
2
3
  /**
3
4
  * Artifact source must be self-contained: allowing a relative import to escape
@@ -7,8 +8,8 @@ import { isAbsolute, relative, resolve, sep } from 'node:path';
7
8
  * allowed because they are external to the artifact by design.
8
9
  */
9
10
  export function rejectOutsideSourcePlugin({ root, trustedExternalRoots, }) {
10
- const rootResolved = resolve(root);
11
- const trustedRoots = trustedExternalRoots.map((trustedRoot) => resolve(trustedRoot));
11
+ const rootResolved = realpathSync(resolve(root));
12
+ const trustedRoots = trustedExternalRoots.map((trustedRoot) => realpathSync(resolve(trustedRoot)));
12
13
  return {
13
14
  name: 'reject-artifact-outside-source',
14
15
  enforce: 'pre',
@@ -17,15 +18,22 @@ export function rejectOutsideSourcePlugin({ root, trustedExternalRoots, }) {
17
18
  if (!resolved || resolved.external || resolved.id.startsWith('\0'))
18
19
  return resolved;
19
20
  const filePath = resolved.id.split('?')[0];
20
- if (!isAbsolute(filePath) || isPathWithin(rootResolved, filePath))
21
+ if (!isAbsolute(filePath))
22
+ return resolved;
23
+ // Vite may return a physical path for a root reached through /tmp or
24
+ // another symlink. Check physical paths on both sides of the boundary;
25
+ // this also prevents an in-root symlink from admitting an outside file.
26
+ const realFilePath = realpathSync(filePath);
27
+ if (isPathWithin(rootResolved, realFilePath))
21
28
  return resolved;
22
29
  const importerPath = importer?.split('?')[0];
23
30
  const importerIsTrustedExternal = importerPath != null &&
24
- (isNodeModulesPath(importerPath) || isTrustedExternalPath(importerPath, trustedRoots));
31
+ (isNodeModulesPath(importerPath) ||
32
+ (isAbsolute(importerPath) && isTrustedExternalPath(realpathSync(importerPath), trustedRoots)));
25
33
  if (isNodeModulesPath(filePath) && (isBareImport(source) || importerIsTrustedExternal)) {
26
34
  return resolved;
27
35
  }
28
- if (isTrustedExternalPath(filePath, trustedRoots))
36
+ if (isTrustedExternalPath(realFilePath, trustedRoots))
29
37
  return resolved;
30
38
  const from = importer
31
39
  ? ` (imported from ${relative(rootResolved, importer.split('?')[0])})`
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {},
12
12
  "devDependencies": {
13
- "@sequenceholdings/artifact-studio": "^0.7.0",
13
+ "@sequenceholdings/artifact-studio": "^0.8.0",
14
14
  "@sequenceholdings/atlas-ui": "^3.1.1",
15
15
  "@tailwindcss/postcss": "^4.2.2",
16
16
  "@tailwindcss/vite": "^4.2.2",
@@ -30,6 +30,8 @@ export interface GatewayAuthorization {
30
30
  publicOrigin: string;
31
31
  /** Signed, audience-scoped context minted by Atlas, never a user bearer. */
32
32
  requestContext?: string;
33
+ /** Generated from the authenticated gateway session, never forwarded from a browser. */
34
+ requestAuthentication?: string;
33
35
  }
34
36
  export interface TunnelBrokerOptions {
35
37
  nodeId: string;
@@ -52,6 +54,10 @@ export declare class ArtifactTunnelBroker {
52
54
  private readonly pendingConnections;
53
55
  private readonly ownerTimers;
54
56
  private closing;
57
+ private draining;
58
+ private readonly drainWaiters;
59
+ private readonly pendingReceipts;
60
+ private relayClosed?;
55
61
  constructor(options: TunnelBrokerOptions);
56
62
  handleTunnelUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): Promise<void>;
57
63
  handleHttp(request: IncomingMessage, response: ServerResponse, sessionId: string): Promise<void>;
@@ -59,6 +65,7 @@ export declare class ArtifactTunnelBroker {
59
65
  handleExternalHttp(input: TunnelExternalRequest): Promise<void>;
60
66
  handleWebSocketUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer, sessionId: string): Promise<void>;
61
67
  private authorize;
68
+ private requestHeaders;
62
69
  /**
63
70
  * The live tunnel for an external request: the session's current lease, for the granted project
64
71
  * and environment, still valid. Null otherwise. No browser owner is compared: Atlas's grant
@@ -79,7 +86,13 @@ export declare class ArtifactTunnelBroker {
79
86
  private writeExternalChunk;
80
87
  private finish;
81
88
  private abort;
82
- /** Immediate revocation and graceful service shutdown hook. */
89
+ /** Stop new upgrades and remote-owner admission while admitted HTTP handlers finish. */
90
+ beginDrain(): void;
91
+ private notifyDrained;
92
+ /** The gateway first joins admitted handlers and response accounting. Its outer deadline
93
+ * bounds finite streams; lease validation and revocation remain active until cleanup. */
94
+ drain(): Promise<void>;
95
+ /** Immediate security revocation, never a graceful drain. */
83
96
  disconnect(sessionId: string): void;
84
97
  close(): void;
85
98
  }