@sequenceholdings/artifact-studio 0.5.0 → 0.6.2

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 (65) hide show
  1. package/README.md +43 -0
  2. package/VERSION-PIN.md +27 -0
  3. package/dist/cli.js +1 -1
  4. package/dist/manifest-next-ingress.d.ts +213 -0
  5. package/dist/manifest-next-ingress.js +187 -0
  6. package/dist/manifest-next-reach.d.ts +40 -0
  7. package/dist/manifest-next-reach.js +48 -0
  8. package/dist/manifest-next.d.ts +133 -8
  9. package/dist/manifest-next.js +7 -46
  10. package/dist/manifest-static.d.ts +6 -0
  11. package/dist/manifest-static.js +10 -0
  12. package/dist/manifest.d.ts +67 -0
  13. package/dist/manifest.js +7 -0
  14. package/dist/offline/connector.d.ts +2 -0
  15. package/dist/offline/connector.js +6 -2
  16. package/dist/offline/management-types.d.ts +25 -0
  17. package/dist/offline/management-types.js +1 -0
  18. package/dist/runtime-contracts.js +1 -1
  19. package/dist/sdk.d.ts +8 -1
  20. package/dist/server/api-client.d.ts +27 -0
  21. package/dist/server/api-client.js +39 -0
  22. package/dist/server/broker-contract.d.ts +33 -6
  23. package/dist/server/broker-contract.js +20 -2
  24. package/dist/server/client.d.ts +32 -45
  25. package/dist/server/client.js +72 -101
  26. package/dist/server/connection-retry.d.ts +12 -0
  27. package/dist/server/connection-retry.js +40 -0
  28. package/dist/server/errors.d.ts +37 -2
  29. package/dist/server/errors.js +66 -6
  30. package/dist/server/external-context.d.ts +21 -0
  31. package/dist/server/external-context.js +19 -0
  32. package/dist/server/ingress-control-contract.d.ts +272 -0
  33. package/dist/server/ingress-control-contract.js +196 -0
  34. package/dist/server/ingress-routes.d.ts +76 -0
  35. package/dist/server/ingress-routes.js +150 -0
  36. package/dist/server/internal/signed-token.d.ts +15 -0
  37. package/dist/server/internal/signed-token.js +67 -0
  38. package/dist/server/internal/v1-context.d.ts +18 -0
  39. package/dist/server/internal/v1-context.js +26 -0
  40. package/dist/server/internal/verify-any.d.ts +22 -0
  41. package/dist/server/internal/verify-any.js +61 -0
  42. package/dist/server/logger.d.ts +1 -1
  43. package/dist/server/principal.d.ts +48 -0
  44. package/dist/server/principal.js +39 -0
  45. package/dist/server/processes.d.ts +56 -0
  46. package/dist/server/processes.js +13 -0
  47. package/dist/server/request-context.d.ts +121 -5
  48. package/dist/server/request-context.js +105 -34
  49. package/dist/server/runtime-config.d.ts +1 -1
  50. package/dist/server/testing.d.ts +40 -0
  51. package/dist/server/testing.js +86 -0
  52. package/dist/server.d.ts +9 -4
  53. package/dist/server.js +5 -2
  54. package/dist/templates/nextjs/README.md +9 -0
  55. package/dist/templates/nextjs/package.json +3 -1
  56. package/dist/templates/nextjs/tests/server.test.mjs +25 -0
  57. package/dist/templates/react-vite/package.json +1 -1
  58. package/dist/tunnel/protocol.js +2 -4
  59. package/dist/tunnel/reserved-headers.d.ts +7 -0
  60. package/dist/tunnel/reserved-headers.js +20 -0
  61. package/package.json +19 -3
  62. package/templates/nextjs/README.md +9 -0
  63. package/templates/nextjs/package.json +3 -1
  64. package/templates/nextjs/tests/server.test.mjs +25 -0
  65. package/templates/react-vite/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { NextArtifactBridgeBootstrap, NextArtifactInitialContext } from '../next/protocol.js';
3
+ import { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX } from './internal/signed-token.js';
3
4
  export declare const ARTIFACT_REQUEST_CONTEXT_HEADER = "x-sequence-artifact-context";
4
5
  export declare const ARTIFACT_RUNTIME_TOKEN_HEADER = "x-sequence-artifact-runtime";
5
6
  export declare const ARTIFACT_SERVER_REQUEST_PATH = "/api/artifact-studio/server/request";
@@ -10,6 +11,8 @@ export declare const ARTIFACT_CONTEXT_MAX_AGE_SECONDS = 60;
10
11
  * window applies only when the signed context names a Lattice run.
11
12
  */
12
13
  export declare const ARTIFACT_LATTICE_CONTEXT_MAX_AGE_SECONDS = 300;
14
+ /** An exact origin: HTTPS, or HTTP on a loopback host. */
15
+ export declare const artifactOriginSchema: z.ZodString;
13
16
  /** Public request context. No bearer credentials or secret values belong here. */
14
17
  export declare const artifactRequestContextSchema: z.ZodObject<{
15
18
  v: z.ZodLiteral<1>;
@@ -30,10 +33,10 @@ export declare const artifactRequestContextSchema: z.ZodObject<{
30
33
  method: z.ZodEnum<{
31
34
  GET: "GET";
32
35
  POST: "POST";
33
- DELETE: "DELETE";
34
- HEAD: "HEAD";
35
36
  PUT: "PUT";
36
37
  PATCH: "PATCH";
38
+ DELETE: "DELETE";
39
+ HEAD: "HEAD";
37
40
  OPTIONS: "OPTIONS";
38
41
  }>;
39
42
  path: z.ZodString;
@@ -61,10 +64,123 @@ export declare const artifactRequestContextSchema: z.ZodObject<{
61
64
  }, z.core.$strict>;
62
65
  export type ArtifactRequestContext = z.infer<typeof artifactRequestContextSchema>;
63
66
  export type ArtifactGatewayPublicKeys = Readonly<Record<string, string>>;
67
+ export { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX };
68
+ /**
69
+ * Prefix of a platform service account's `sub`. Mirrors Atlas's `SERVICE_ACCOUNT_SUB_PREFIX`
70
+ * (atlas/src/server/lib/auth0-sub.ts); Atlas's auth0-sub test pins the two together.
71
+ */
72
+ export declare const ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX = "service-account|";
73
+ /** An external context's lifetime cap. External contexts never carry a Lattice run. */
74
+ export declare const ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS = 60;
75
+ /** An IPv4 or IPv6 address, as the gateway observed it. The control contract reuses it. */
76
+ export declare const artifactIpAddressSchema: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
77
+ /** The external caller as the gateway verified it at pass exchange. Strict: see the v2 key policy below. */
78
+ export declare const artifactExternalPrincipalSchema: z.ZodObject<{
79
+ kind: z.ZodLiteral<"external">;
80
+ ingress: z.ZodString;
81
+ provider: z.ZodString;
82
+ issuer: z.ZodString;
83
+ subject: z.ZodString;
84
+ clientId: z.ZodString;
85
+ email: z.ZodString;
86
+ emailVerified: z.ZodLiteral<true>;
87
+ authTime: z.ZodNullable<z.ZodNumber>;
88
+ amr: z.ZodNullable<z.ZodArray<z.ZodString>>;
89
+ tokenIssuedAt: z.ZodNumber;
90
+ pass: z.ZodObject<{
91
+ id: z.ZodUUID;
92
+ kid: z.ZodString;
93
+ issuedAt: z.ZodNumber;
94
+ exchangeIp: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
95
+ }, z.core.$strict>;
96
+ client: z.ZodObject<{
97
+ ip: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
98
+ userAgent: z.ZodString;
99
+ userAgentSha256: z.ZodString;
100
+ origin: z.ZodNullable<z.ZodString>;
101
+ }, z.core.$strict>;
102
+ }, z.core.$strict>;
103
+ /**
104
+ * External request context (v2): a caller that is not an Atlas user, admitted through a
105
+ * manifest-declared ingress. Key policy: any unknown top-level or `principal` key is refused
106
+ * unless it sits under `ext`. `crit` is reserved for extensions a verifier must understand; this
107
+ * verifier understands none, so it must be empty. Security-relevant additions bump `v`.
108
+ *
109
+ * Public verifiers ({@link verifyArtifactRequestContext}) never accept this version; only the
110
+ * explicit external APIs do.
111
+ */
112
+ export declare const requestContextV2ExternalSchema: z.ZodObject<{
113
+ v: z.ZodLiteral<2>;
114
+ iss: z.ZodLiteral<"atlas-artifact-gateway">;
115
+ aud: z.ZodString;
116
+ sub: z.ZodString;
117
+ projectId: z.ZodString;
118
+ environment: z.ZodString;
119
+ mode: z.ZodEnum<{
120
+ published: "published";
121
+ development: "development";
122
+ }>;
123
+ deploymentId: z.ZodNullable<z.ZodString>;
124
+ devSessionId: z.ZodNullable<z.ZodString>;
125
+ requestId: z.ZodString;
126
+ jti: z.ZodUUID;
127
+ method: z.ZodEnum<{
128
+ GET: "GET";
129
+ POST: "POST";
130
+ PUT: "PUT";
131
+ PATCH: "PATCH";
132
+ DELETE: "DELETE";
133
+ HEAD: "HEAD";
134
+ OPTIONS: "OPTIONS";
135
+ }>;
136
+ path: z.ZodString;
137
+ iat: z.ZodNumber;
138
+ exp: z.ZodNumber;
139
+ serviceAccountId: z.ZodString;
140
+ principal: z.ZodObject<{
141
+ kind: z.ZodLiteral<"external">;
142
+ ingress: z.ZodString;
143
+ provider: z.ZodString;
144
+ issuer: z.ZodString;
145
+ subject: z.ZodString;
146
+ clientId: z.ZodString;
147
+ email: z.ZodString;
148
+ emailVerified: z.ZodLiteral<true>;
149
+ authTime: z.ZodNullable<z.ZodNumber>;
150
+ amr: z.ZodNullable<z.ZodArray<z.ZodString>>;
151
+ tokenIssuedAt: z.ZodNumber;
152
+ pass: z.ZodObject<{
153
+ id: z.ZodUUID;
154
+ kid: z.ZodString;
155
+ issuedAt: z.ZodNumber;
156
+ exchangeIp: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
157
+ }, z.core.$strict>;
158
+ client: z.ZodObject<{
159
+ ip: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
160
+ userAgent: z.ZodString;
161
+ userAgentSha256: z.ZodString;
162
+ origin: z.ZodNullable<z.ZodString>;
163
+ }, z.core.$strict>;
164
+ }, z.core.$strict>;
165
+ crit: z.ZodArray<z.ZodString>;
166
+ ext: z.ZodRecord<z.ZodString, z.ZodUnknown>;
167
+ }, z.core.$strict>;
168
+ export type RequestContextV2External = z.infer<typeof requestContextV2ExternalSchema>;
169
+ /** Why a context was refused, when the refusal has a name a handler can act on. */
170
+ export type ArtifactContextErrorReason = 'external_context_not_accepted' | 'external_context_required' | 'external_ingress_not_accepted' | 'external_mode_not_accepted';
64
171
  export declare class ArtifactContextError extends Error {
65
- constructor();
172
+ readonly reason: ArtifactContextErrorReason | undefined;
173
+ constructor(reason?: ArtifactContextErrorReason);
66
174
  }
67
- /** Only the gateway owns private keys. Applications pin platform-provided public keys. */
175
+ /**
176
+ * Only the gateway owns private keys. Applications pin platform-provided public keys.
177
+ *
178
+ * Accepts v1 (Atlas user and platform service) contexts only. An external (v2) context, or any
179
+ * token signed with an `external-` kid, is refused with reason `external_context_not_accepted`
180
+ * before any other check, so a borrower context can never reach a handler that reads `sub` and
181
+ * `capabilities` as an Atlas user. External contexts are verified only through
182
+ * `verifyArtifactExternalContext` or `createArtifactServerClient({ accept })`.
183
+ */
68
184
  export declare function verifyArtifactRequestContext({ token, publicKeys, projectId, environment, method, path, now }: {
69
185
  token: string | null;
70
186
  publicKeys: ArtifactGatewayPublicKeys;
@@ -76,6 +192,6 @@ export declare function verifyArtifactRequestContext({ token, publicKeys, projec
76
192
  now?: number;
77
193
  }): ArtifactRequestContext;
78
194
  /** Safe props for the client bridge; intentionally excludes actor and runtime credentials. */
79
- export declare function getArtifactBrowserBootstrap(context: ArtifactRequestContext): NextArtifactBridgeBootstrap & {
195
+ export declare function getArtifactBrowserBootstrap(context: ArtifactRequestContext | RequestContextV2External): NextArtifactBridgeBootstrap & {
80
196
  initialContext: NextArtifactInitialContext;
81
197
  };
@@ -1,6 +1,7 @@
1
- import { createPublicKey, verify } from 'node:crypto';
2
1
  import { z } from 'zod';
3
2
  import { isLoopbackHostname } from '../loopback.js';
3
+ import { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX, isExternalContextToken, verifySignedContext } from './internal/signed-token.js';
4
+ import { parseV1Context } from './internal/v1-context.js';
4
5
  export const ARTIFACT_REQUEST_CONTEXT_HEADER = 'x-sequence-artifact-context';
5
6
  export const ARTIFACT_RUNTIME_TOKEN_HEADER = 'x-sequence-artifact-runtime';
6
7
  export const ARTIFACT_SERVER_REQUEST_PATH = '/api/artifact-studio/server/request';
@@ -12,7 +13,8 @@ export const ARTIFACT_CONTEXT_MAX_AGE_SECONDS = 60;
12
13
  */
13
14
  export const ARTIFACT_LATTICE_CONTEXT_MAX_AGE_SECONDS = 300;
14
15
  const id = z.string().min(1).max(255);
15
- const originSchema = z.string().max(2048).refine(value => {
16
+ /** An exact origin: HTTPS, or HTTP on a loopback host. */
17
+ export const artifactOriginSchema = z.string().max(2048).refine(value => {
16
18
  try {
17
19
  const url = new URL(value);
18
20
  return value === url.origin && (url.protocol === 'https:' ||
@@ -45,7 +47,7 @@ export const artifactRequestContextSchema = z.object({
45
47
  exp: z.number().int().positive(),
46
48
  capabilities: z.array(id).max(256),
47
49
  serviceAccountId: id.nullable(),
48
- bridge: z.object({ parentOrigin: originSchema, channelId: id, nonce: z.string().min(16).max(255) }).strict(),
50
+ bridge: z.object({ parentOrigin: artifactOriginSchema, channelId: id, nonce: z.string().min(16).max(255) }).strict(),
49
51
  theme: z.enum(['light', 'dark', 'system']),
50
52
  lattice: z.object({ runId: id.nullable(), nodeId: id.nullable(), stepId: id.nullable(), processId: id.nullable() }).strict(),
51
53
  taskRoutes: z.array(z.string().min(1).max(512)).max(16).refine(routes => routes.join('').length <= 1024).optional(),
@@ -60,49 +62,118 @@ export const artifactRequestContextSchema = z.object({
60
62
  if (value.mode === 'development' ? !value.devSessionId || value.deploymentId !== null : !value.deploymentId || value.devSessionId !== null)
61
63
  ctx.addIssue({ code: 'custom', message: 'Artifact runtime identity does not match its mode' });
62
64
  });
65
+ export { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX };
66
+ /**
67
+ * Prefix of a platform service account's `sub`. Mirrors Atlas's `SERVICE_ACCOUNT_SUB_PREFIX`
68
+ * (atlas/src/server/lib/auth0-sub.ts); Atlas's auth0-sub test pins the two together.
69
+ */
70
+ export const ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX = 'service-account|';
71
+ /** An external context's lifetime cap. External contexts never carry a Lattice run. */
72
+ export const ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS = 60;
73
+ /** An IPv4 or IPv6 address, as the gateway observed it. The control contract reuses it. */
74
+ export const artifactIpAddressSchema = z.union([z.ipv4(), z.ipv6()]);
75
+ /** Signed by the gateway after it strips control characters and truncates; refused here otherwise. */
76
+ const observedUserAgent = z.string().max(512).refine(value => !/[\u0000-\u001f\u007f]/.test(value), 'Expected a user agent without control characters');
77
+ /** The external caller as the gateway verified it at pass exchange. Strict: see the v2 key policy below. */
78
+ export const artifactExternalPrincipalSchema = z.object({
79
+ kind: z.literal('external'),
80
+ /** Routing label for the manifest's ingress declaration. Not an authorization attribute. */
81
+ ingress: id,
82
+ provider: id,
83
+ issuer: z.string().min(1).max(2048),
84
+ subject: id,
85
+ clientId: id,
86
+ /** Normalized (trimmed, lowercase) by Atlas at exchange. */
87
+ email: z.string().min(3).max(320).refine(value => value === value.trim().toLowerCase() && value.includes('@'), 'Expected a normalized email'),
88
+ emailVerified: z.literal(true),
89
+ authTime: z.number().int().nonnegative().nullable(),
90
+ amr: z.array(z.string().min(1).max(64)).max(16).nullable(),
91
+ tokenIssuedAt: z.number().int().nonnegative(),
92
+ pass: z.object({ id: z.uuid(), kid: id, issuedAt: z.number().int().nonnegative(), exchangeIp: artifactIpAddressSchema }).strict(),
93
+ client: z.object({
94
+ ip: artifactIpAddressSchema,
95
+ userAgent: observedUserAgent,
96
+ userAgentSha256: z.string().regex(/^[0-9a-f]{64}$/),
97
+ origin: artifactOriginSchema.nullable(),
98
+ }).strict(),
99
+ }).strict();
100
+ /**
101
+ * External request context (v2): a caller that is not an Atlas user, admitted through a
102
+ * manifest-declared ingress. Key policy: any unknown top-level or `principal` key is refused
103
+ * unless it sits under `ext`. `crit` is reserved for extensions a verifier must understand; this
104
+ * verifier understands none, so it must be empty. Security-relevant additions bump `v`.
105
+ *
106
+ * Public verifiers ({@link verifyArtifactRequestContext}) never accept this version; only the
107
+ * explicit external APIs do.
108
+ */
109
+ export const requestContextV2ExternalSchema = z.object({
110
+ v: z.literal(2),
111
+ iss: z.literal('atlas-artifact-gateway'),
112
+ aud: z.string().min(1).max(300),
113
+ sub: id.refine(value => value.startsWith('ext|'), 'External subjects are namespaced ext|<provider>|<subject>'),
114
+ projectId: id,
115
+ environment: id,
116
+ mode: z.enum(['published', 'development']),
117
+ deploymentId: id.nullable(),
118
+ devSessionId: id.nullable(),
119
+ requestId: id,
120
+ jti: z.uuid(),
121
+ method: artifactRequestContextSchema.shape.method,
122
+ path: artifactRequestContextSchema.shape.path,
123
+ iat: z.number().int().nonnegative(),
124
+ exp: z.number().int().positive(),
125
+ /** The ingress's own service account; external principals act only through service mode. */
126
+ serviceAccountId: id,
127
+ principal: artifactExternalPrincipalSchema,
128
+ crit: z.array(z.string()).length(0),
129
+ ext: z.record(z.string().min(1).max(64), z.unknown()),
130
+ }).strict().superRefine((value, ctx) => {
131
+ if (value.aud !== `artifact:${value.projectId}`)
132
+ ctx.addIssue({ code: 'custom', message: 'Invalid Artifact audience' });
133
+ if (value.sub !== `ext|${value.principal.provider}|${value.principal.subject}`)
134
+ ctx.addIssue({ code: 'custom', message: 'External subject does not match its principal' });
135
+ if (value.exp <= value.iat || value.exp - value.iat > ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS)
136
+ ctx.addIssue({ code: 'custom', message: 'Invalid Artifact context lifetime' });
137
+ if (value.mode === 'development' ? !value.devSessionId || value.deploymentId !== null : !value.deploymentId || value.devSessionId !== null)
138
+ ctx.addIssue({ code: 'custom', message: 'Artifact runtime identity does not match its mode' });
139
+ });
63
140
  export class ArtifactContextError extends Error {
64
- constructor() { super('Missing or invalid Artifact gateway request context'); this.name = 'ArtifactContextError'; }
65
- }
66
- function decode(value) {
67
- if (!/^[A-Za-z0-9_-]+$/.test(value))
68
- throw new ArtifactContextError();
69
- return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
141
+ reason;
142
+ constructor(reason) {
143
+ super('Missing or invalid Artifact gateway request context');
144
+ this.name = 'ArtifactContextError';
145
+ this.reason = reason;
146
+ }
70
147
  }
71
- /** Only the gateway owns private keys. Applications pin platform-provided public keys. */
148
+ /**
149
+ * Only the gateway owns private keys. Applications pin platform-provided public keys.
150
+ *
151
+ * Accepts v1 (Atlas user and platform service) contexts only. An external (v2) context, or any
152
+ * token signed with an `external-` kid, is refused with reason `external_context_not_accepted`
153
+ * before any other check, so a borrower context can never reach a handler that reads `sub` and
154
+ * `capabilities` as an Atlas user. External contexts are verified only through
155
+ * `verifyArtifactExternalContext` or `createArtifactServerClient({ accept })`.
156
+ */
72
157
  export function verifyArtifactRequestContext({ token, publicKeys, projectId, environment, method, path, now = Math.floor(Date.now() / 1000) }) {
158
+ if (isExternalContextToken(token))
159
+ throw new ArtifactContextError('external_context_not_accepted');
160
+ let payload;
73
161
  try {
74
- if (!token || token.length > 32768)
75
- throw new ArtifactContextError();
76
- const parts = token.split('.');
77
- if (parts.length !== 3)
78
- throw new ArtifactContextError();
79
- const [encodedHeader, encodedPayload, signature] = parts;
80
- const header = z.object({ alg: z.literal('EdDSA'), typ: z.literal('JWT'), kid: id }).strict().parse(decode(encodedHeader));
81
- const pem = Object.hasOwn(publicKeys, header.kid) ? publicKeys[header.kid] : undefined;
82
- if (!pem || !/^[A-Za-z0-9_-]+$/.test(signature))
83
- throw new ArtifactContextError();
84
- const key = createPublicKey(pem);
85
- if (key.asymmetricKeyType !== 'ed25519' ||
86
- !verify(null, Buffer.from(`${encodedHeader}.${encodedPayload}`), key, Buffer.from(signature, 'base64url')))
87
- throw new ArtifactContextError();
88
- const context = artifactRequestContextSchema.parse(decode(encodedPayload));
89
- if (context.projectId !== projectId || context.environment !== environment ||
90
- context.exp <= now || context.iat > now + 5 ||
91
- (method !== undefined && context.method !== method.toUpperCase()) || (path !== undefined && context.path !== path))
162
+ const verified = verifySignedContext({ token, publicKeys });
163
+ if (verified.kid.startsWith(ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX))
92
164
  throw new ArtifactContextError();
93
- Object.freeze(context.bridge);
94
- Object.freeze(context.lattice);
95
- Object.freeze(context.capabilities);
96
- if (context.taskRoutes)
97
- Object.freeze(context.taskRoutes);
98
- return Object.freeze(context);
165
+ payload = verified.payload;
99
166
  }
100
167
  catch {
101
168
  throw new ArtifactContextError();
102
169
  }
170
+ return parseV1Context({ payload, projectId, environment, method, path, now });
103
171
  }
104
172
  /** Safe props for the client bridge; intentionally excludes actor and runtime credentials. */
105
173
  export function getArtifactBrowserBootstrap(context) {
174
+ // An external caller has no Atlas bridge, theme or Lattice run to hand to a browser.
175
+ if (context.v !== 1)
176
+ throw new ArtifactContextError('external_context_not_accepted');
106
177
  return {
107
178
  parentOrigin: context.bridge.parentOrigin,
108
179
  channelId: context.bridge.channelId,
@@ -32,7 +32,7 @@ export declare function getArtifactRequestContext({ headers, runtime, method, pa
32
32
  devSessionId: string | null;
33
33
  requestId: string;
34
34
  jti: string;
35
- method: "GET" | "POST" | "DELETE" | "HEAD" | "PUT" | "PATCH" | "OPTIONS";
35
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
36
36
  path: string;
37
37
  iat: number;
38
38
  exp: number;
@@ -0,0 +1,40 @@
1
+ import { type KeyObject } from 'node:crypto';
2
+ import { type RequestContextV2External } from './request-context.js';
3
+ import { type ArtifactRuntimeConfig } from './runtime-config.js';
4
+ /**
5
+ * Test helpers for applications that accept external callers: sign an external (v2) context
6
+ * with a test key, and run code against an explicit runtime configuration. For unit tests and
7
+ * local `next dev` only. A context signed here verifies only against the matching public key,
8
+ * which a hosted runtime never pins.
9
+ */
10
+ type ExternalPrincipalClaims = RequestContextV2External['principal'];
11
+ export interface SignTestExternalContextInput {
12
+ privateKey: KeyObject | string;
13
+ /** Use an `external-` kid; verifiers refuse any other kid on an external context. */
14
+ kid: string;
15
+ projectId: string;
16
+ environment: string;
17
+ method: string;
18
+ path: string;
19
+ /** Overrides on a verified-borrower default principal. */
20
+ principal?: Partial<Omit<ExternalPrincipalClaims, 'kind'>>;
21
+ serviceAccountId: string;
22
+ /** Defaults to `published`. Verifiers refuse `development` until development leases admit it. */
23
+ mode?: RequestContextV2External['mode'];
24
+ deploymentId?: string | null;
25
+ devSessionId?: string | null;
26
+ /** Seconds since the epoch; defaults to now. */
27
+ now?: number;
28
+ /** Defaults to and may not exceed 60. */
29
+ ttlSeconds?: number;
30
+ /** Overrides applied after the defaults, unvalidated, for negative tests. */
31
+ claims?: Record<string, unknown>;
32
+ }
33
+ export declare function signTestExternalContext({ privateKey, kid, projectId, environment, method, path, principal, serviceAccountId, mode, deploymentId, devSessionId, now, ttlSeconds, claims, }: SignTestExternalContextInput): string;
34
+ /**
35
+ * Runs `fn` with the hosted runtime configuration set to `config` (and no runtime file), then
36
+ * restores the previous environment. The override is process-wide: do not run overlapping calls.
37
+ * Refuses to run in a production build.
38
+ */
39
+ export declare function withTestRuntimeConfig<T>(config: ArtifactRuntimeConfig, fn: () => Promise<T> | T): Promise<T>;
40
+ export {};
@@ -0,0 +1,86 @@
1
+ import { createHash, createPrivateKey, randomUUID, sign } from 'node:crypto';
2
+ import { ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS, requestContextV2ExternalSchema } from './request-context.js';
3
+ import { artifactRuntimeConfigSchema } from './runtime-config.js';
4
+ const defaultUserAgent = 'Mozilla/5.0 (test)';
5
+ export function signTestExternalContext({ privateKey, kid, projectId, environment, method, path, principal = {}, serviceAccountId, mode = 'published', deploymentId, devSessionId, now = Math.floor(Date.now() / 1000), ttlSeconds = ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS, claims = {}, }) {
6
+ const userAgent = principal.client?.userAgent ?? defaultUserAgent;
7
+ const resolvedPrincipal = {
8
+ kind: 'external',
9
+ ingress: 'borrower',
10
+ provider: 'test-provider',
11
+ issuer: 'https://idp.example.test/',
12
+ subject: 'test|borrower-1',
13
+ clientId: 'test-client',
14
+ email: 'borrower@example.test',
15
+ emailVerified: true,
16
+ authTime: now - 30,
17
+ amr: ['pwd'],
18
+ tokenIssuedAt: now - 30,
19
+ ...principal,
20
+ pass: { id: randomUUID(), kid: 'pass-1', issuedAt: now - 10, exchangeIp: '203.0.113.10', ...principal.pass },
21
+ client: {
22
+ ip: '203.0.113.10',
23
+ userAgentSha256: createHash('sha256').update(userAgent).digest('hex'),
24
+ origin: 'https://borrower.example.test',
25
+ ...principal.client,
26
+ userAgent,
27
+ },
28
+ };
29
+ const published = mode === 'published';
30
+ const payload = {
31
+ v: 2,
32
+ iss: 'atlas-artifact-gateway',
33
+ aud: `artifact:${projectId}`,
34
+ sub: `ext|${resolvedPrincipal.provider}|${resolvedPrincipal.subject}`,
35
+ projectId,
36
+ environment,
37
+ mode,
38
+ deploymentId: deploymentId !== undefined ? deploymentId : published ? 'test-deployment' : null,
39
+ devSessionId: devSessionId !== undefined ? devSessionId : published ? null : 'test-dev-session',
40
+ requestId: randomUUID(),
41
+ jti: randomUUID(),
42
+ method: method.toUpperCase(),
43
+ path,
44
+ iat: now,
45
+ exp: now + ttlSeconds,
46
+ serviceAccountId,
47
+ principal: resolvedPrincipal,
48
+ crit: [],
49
+ ext: {},
50
+ ...claims,
51
+ };
52
+ // Catch a mistaken default early; `claims` overrides are for deliberately invalid contexts.
53
+ if (Object.keys(claims).length === 0)
54
+ requestContextV2ExternalSchema.parse(payload);
55
+ const key = typeof privateKey === 'string' ? createPrivateKey(privateKey) : privateKey;
56
+ const unsigned = [{ alg: 'EdDSA', typ: 'JWT', kid }, payload]
57
+ .map(part => Buffer.from(JSON.stringify(part)).toString('base64url')).join('.');
58
+ return `${unsigned}.${sign(null, Buffer.from(unsigned), key).toString('base64url')}`;
59
+ }
60
+ const runtimeEnvironmentKeys = ['SEQUENCE_ARTIFACT_RUNTIME_FILE', 'SEQUENCE_ARTIFACT_RUNTIME_CONFIG', 'SEQUENCE_ARTIFACT_RUNTIME_TOKEN'];
61
+ /**
62
+ * Runs `fn` with the hosted runtime configuration set to `config` (and no runtime file), then
63
+ * restores the previous environment. The override is process-wide: do not run overlapping calls.
64
+ * Refuses to run in a production build.
65
+ */
66
+ export async function withTestRuntimeConfig(config, fn) {
67
+ if (process.env.NODE_ENV === 'production')
68
+ throw new Error('withTestRuntimeConfig is for tests and local development only');
69
+ const { sessionToken, ...hosted } = artifactRuntimeConfigSchema.parse(config);
70
+ const previous = Object.fromEntries(runtimeEnvironmentKeys.map(key => [key, process.env[key]]));
71
+ delete process.env.SEQUENCE_ARTIFACT_RUNTIME_FILE;
72
+ process.env.SEQUENCE_ARTIFACT_RUNTIME_CONFIG = JSON.stringify(hosted);
73
+ process.env.SEQUENCE_ARTIFACT_RUNTIME_TOKEN = sessionToken;
74
+ try {
75
+ return await fn();
76
+ }
77
+ finally {
78
+ for (const key of runtimeEnvironmentKeys) {
79
+ const value = previous[key];
80
+ if (value === undefined)
81
+ delete process.env[key];
82
+ else
83
+ process.env[key] = value;
84
+ }
85
+ }
86
+ }
package/dist/server.d.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  import 'server-only';
2
- export { createArtifactServerClient, ArtifactServerError } from './server/client.js';
2
+ export { createArtifactServerClient } from './server/client.js';
3
+ export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './server/errors.js';
3
4
  export type { ArtifactAtlasRefusal } from './server/errors.js';
4
- export type { ArtifactAuditEvent, ArtifactOrmOperations, ArtifactOrmOptions, ArtifactProcessAdvance, ArtifactProcessAdvanceResult, ArtifactProcessArtifactRef, ArtifactProcessAssign, ArtifactProcessAssignResult, ArtifactProcessDefinition, ArtifactProcessDescribeQuery, ArtifactProcessEdge, ArtifactProcessHumanNode, ArtifactProcessNode, ArtifactProcessNodeBase, ArtifactProcessNodeState, ArtifactProcessPrincipal, ArtifactProcessPrincipalSearch, ArtifactProcessPrincipals, ArtifactProcessRun, ArtifactProcessRunRecord, ArtifactProcessRunSnapshot, ArtifactProcessRunsQuery, ArtifactProcessRunViewerChange, ArtifactProcessRunViewers, ArtifactProcessStart, ArtifactProcessStepRecord, ArtifactProcessTask, ArtifactProcessTasksQuery, ArtifactServerAudit, ArtifactServerClient, ArtifactServerFetchOptions, ArtifactServerFlags, ArtifactServerProcesses, } from './server/client.js';
5
+ export type { ArtifactAuditEvent, ArtifactOrmOperations, ArtifactOrmOptions, ArtifactProcessAdvance, ArtifactProcessAdvanceResult, ArtifactProcessArtifactRef, ArtifactProcessAssign, ArtifactProcessAssignResult, ArtifactProcessDefinition, ArtifactProcessDescribeQuery, ArtifactProcessEdge, ArtifactProcessHumanNode, ArtifactProcessNode, ArtifactProcessNodeBase, ArtifactProcessNodeState, ArtifactProcessPrincipal, ArtifactProcessPrincipalSearch, ArtifactProcessPrincipals, ArtifactProcessRun, ArtifactProcessRunRecord, ArtifactProcessRunSnapshot, ArtifactProcessRunsQuery, ArtifactProcessRunViewerChange, ArtifactProcessRunViewers, ArtifactProcessStart, ArtifactProcessStepRecord, ArtifactProcessTask, ArtifactProcessTasksQuery, ArtifactServerAudit, ArtifactServerClient, ArtifactServerClientOptions, ArtifactServerContext, ArtifactServerFetchOptions, ArtifactServerFlags, ArtifactServerProcesses, } from './server/client.js';
5
6
  export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
6
7
  export type { ArtifactAuditAction, ArtifactProcessPrincipalType, ArtifactProcessRunStatus, ArtifactProcessTaskView, } from './server/broker-contract.js';
7
8
  export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
8
9
  export type { ArtifactRuntimeConfig, ArtifactRequestHeaders } from './server/runtime-config.js';
9
- export { getArtifactBrowserBootstrap } from './server/request-context.js';
10
- export type { ArtifactRequestContext } from './server/request-context.js';
10
+ export { ArtifactContextError, getArtifactBrowserBootstrap } from './server/request-context.js';
11
+ export type { ArtifactContextErrorReason, ArtifactRequestContext, RequestContextV2External } from './server/request-context.js';
12
+ export { verifyArtifactExternalContext } from './server/external-context.js';
13
+ export type { ArtifactExternalAccept } from './server/external-context.js';
14
+ export { externalEvidence } from './server/principal.js';
15
+ export type { ArtifactExternalEvidence, ArtifactExternalPrincipal, ArtifactPrincipal } from './server/principal.js';
11
16
  export type { ArtifactLogMetadata, ArtifactLogRecord } from './server/logger.js';
12
17
  export type { ArtifactOrmJsonScalars } from './orm-json-scalars.js';
package/dist/server.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import 'server-only';
2
- export { createArtifactServerClient, ArtifactServerError } from './server/client.js';
2
+ export { createArtifactServerClient } from './server/client.js';
3
+ export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './server/errors.js';
3
4
  export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
4
5
  export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
5
- export { getArtifactBrowserBootstrap } from './server/request-context.js';
6
+ export { ArtifactContextError, getArtifactBrowserBootstrap } from './server/request-context.js';
7
+ export { verifyArtifactExternalContext } from './server/external-context.js';
8
+ export { externalEvidence } from './server/principal.js';
@@ -97,6 +97,11 @@ only runs that user may view, `getRun` needs Viewer on the run and returns
97
97
  every viewer — keep the data your pages need in your own ORM namespace. Reads
98
98
  work from pages and GET route handlers; starting a run does not.
99
99
 
100
+ To follow a run as it moves, poll `processes.listEvents({ runId, since, limit })`
101
+ with the `event_cursor` from `getRun`, then with the `cursor` each page returns:
102
+ every event arrives once, oldest first, in the shape the run's stream sends. It
103
+ needs Viewer on the run, like `getRun`.
104
+
100
105
  The same `processes.read` declaration lets you render the process itself.
101
106
  `processes.describe({ process, version })` returns the applied template a run
102
107
  is filled in from — `{ id, name, version, new_runs_disabled, start_node_id,
@@ -128,6 +133,10 @@ groups })` reassigns a parked step (its advancers become exactly those
128
133
  principals) and `processes.grantViewers` / `processes.revokeViewers({ runId,
129
134
  users, groups })` share a run or take a share back; all three need the user to
130
135
  own the run, and `processes.listViewers(runId)` shows the run's stored shares.
136
+ `processes.cancel(runId)` cancels a run, and `processes.resetStep({ runId,
137
+ stepId })` stops a generating agent step without cancelling its run (the run
138
+ re-parks at the node's reset edge); both need the user's `invoker` relation on
139
+ the run, as the Lattice routes do.
131
140
  Users are Auth0 subs and groups are chat-group ids — take them from
132
141
  `processes.searchPrincipals({ q, types, limit })`, which finds people in the
133
142
  user's own tenant and groups they can see, as the Lattice picker does. The
@@ -10,7 +10,9 @@
10
10
  "dev": "next dev --webpack --hostname 127.0.0.1",
11
11
  "build": "next build",
12
12
  "start": "next start --hostname 127.0.0.1",
13
- "type-check": "next typegen && tsc --noEmit"
13
+ "lint": "next typegen && tsc --noEmit --noUnusedLocals --noUnusedParameters --noFallthroughCasesInSwitch",
14
+ "type-check": "next typegen && tsc --noEmit",
15
+ "test": "node --test tests/*.test.mjs"
14
16
  },
15
17
  "dependencies": {
16
18
  "@sequenceholdings/artifact-studio": "{{artifactStudioVersion}}",
@@ -0,0 +1,25 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import { greet } from '../app/actions.ts'
4
+ import { GET } from '../app/api/health/route.ts'
5
+
6
+ test('greeting validates input and returns the server action result', async () => {
7
+ const data = new FormData()
8
+ data.set('name', ' Atlas ')
9
+ assert.deepEqual(await greet({ message: '', error: null }, data), {
10
+ message: 'Hello, Atlas. Your server action ran successfully.', error: null,
11
+ })
12
+ for (const name of ['', ' '.repeat(3), 'a'.repeat(81)]) {
13
+ data.set('name', name)
14
+ const result = await greet({ message: '', error: null }, data)
15
+ assert.equal(result.message, '')
16
+ assert.equal(result.error, 'Enter a name between 1 and 80 characters.')
17
+ }
18
+ })
19
+
20
+ test('health route returns uncached JSON without any runtime bindings', async () => {
21
+ const response = GET()
22
+ assert.equal(response.status, 200)
23
+ assert.equal(response.headers.get('cache-control'), 'no-store')
24
+ assert.deepEqual(await response.json(), { ok: true, framework: 'nextjs' })
25
+ })
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "dependencies": {},
11
11
  "devDependencies": {
12
- "@sequenceholdings/artifact-studio": "^0.5.0",
12
+ "@sequenceholdings/artifact-studio": "^0.6.0",
13
13
  "@sequenceholdings/atlas-ui": "^3.1.1",
14
14
  "@tailwindcss/postcss": "^4.2.2",
15
15
  "@tailwindcss/vite": "^4.2.2",
@@ -1,6 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { z } from 'zod';
3
3
  import { isLoopbackHostname } from '../loopback.js';
4
+ import { isPrivateRequestHeader } from './reserved-headers.js';
4
5
  /** Version 1 is negotiated by the tunnel connection, not by individual streams. */
5
6
  export const TUNNEL_PROTOCOL = 'artifact-tunnel.v1';
6
7
  export const DEFAULT_TUNNEL_LIMITS = Object.freeze({
@@ -143,10 +144,7 @@ function removePrivateHeaders(headers) {
143
144
  const nominated = new Set((Array.isArray(connection) ? connection.join(',') : connection ?? '')
144
145
  .split(',').map((name) => name.trim().toLowerCase()));
145
146
  for (const name of headers.keys()) {
146
- if (HOP_HEADERS.has(name) || nominated.has(name) || name === 'forwarded' ||
147
- name === 'authorization' || name === 'remote-user' || name === 'x-remote-user' ||
148
- name === 'x-serverless-authorization' || name === 'x-goog-iap-jwt-assertion' ||
149
- /^(?:x-forwarded-|x-sequence-|x-artifact-|x-atlas-|x-user-|x-auth-|x-authenticated-|x-goog-authenticated-|proxy-)/.test(name))
147
+ if (HOP_HEADERS.has(name) || nominated.has(name) || isPrivateRequestHeader(name))
150
148
  headers.delete(name);
151
149
  }
152
150
  }
@@ -0,0 +1,7 @@
1
+ /** Lowercase header name the proxy strips from every request it forwards to an app. */
2
+ export declare function isPrivateRequestHeader(name: string): boolean;
3
+ /**
4
+ * Client-address headers an external ingress drops: on an ingress the client address comes only
5
+ * from the edge-verified `CF-Connecting-IP`, never from a header the caller can set.
6
+ */
7
+ export declare const INGRESS_CLIENT_ADDRESS_HEADERS: readonly string[];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Request headers the platform owns. The proxy strips them before a request reaches app code
3
+ * (`removePrivateHeaders` in `protocol.ts`), so a manifest may never declare one as an app header.
4
+ * Dependency-free: browser code that parses manifests imports it.
5
+ */
6
+ const PRIVATE_REQUEST_HEADER_NAMES = new Set([
7
+ 'forwarded', 'authorization', 'remote-user', 'x-remote-user', 'x-serverless-authorization', 'x-goog-iap-jwt-assertion',
8
+ ]);
9
+ const PRIVATE_REQUEST_HEADER_PREFIX = /^(?:x-forwarded-|x-sequence-|x-artifact-|x-atlas-|x-user-|x-auth-|x-authenticated-|x-goog-authenticated-|proxy-)/;
10
+ /** Lowercase header name the proxy strips from every request it forwards to an app. */
11
+ export function isPrivateRequestHeader(name) {
12
+ return PRIVATE_REQUEST_HEADER_NAMES.has(name) || PRIVATE_REQUEST_HEADER_PREFIX.test(name);
13
+ }
14
+ /**
15
+ * Client-address headers an external ingress drops: on an ingress the client address comes only
16
+ * from the edge-verified `CF-Connecting-IP`, never from a header the caller can set.
17
+ */
18
+ export const INGRESS_CLIENT_ADDRESS_HEADERS = Object.freeze([
19
+ 'true-client-ip', 'x-real-ip', 'x-client-ip', 'client-ip', 'x-cluster-client-ip',
20
+ ]);