@sequenceholdings/artifact-studio 0.4.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +35 -1
  2. package/VERSION-PIN.md +29 -0
  3. package/dist/active-deploy-policy.d.ts +9 -3
  4. package/dist/active-deploy-policy.js +20 -8
  5. package/dist/artifact-env-bindings.d.ts +9 -0
  6. package/dist/artifact-env-bindings.js +32 -0
  7. package/dist/build.js +6 -0
  8. package/dist/cli.js +62 -13
  9. package/dist/deployment-validation.d.ts +2 -0
  10. package/dist/deployment-validation.js +2 -0
  11. package/dist/dev-link.js +28 -2
  12. package/dist/env-binding-scan.d.ts +10 -0
  13. package/dist/env-binding-scan.js +179 -0
  14. package/dist/env-bindings.d.ts +52 -0
  15. package/dist/env-bindings.js +180 -0
  16. package/dist/gateway/control.d.ts +33 -0
  17. package/dist/gateway/control.js +29 -0
  18. package/dist/gateway/hosted-proxy.d.ts +5 -0
  19. package/dist/gateway/hosted-proxy.js +4 -1
  20. package/dist/gateway/lattice-invoke.d.ts +48 -0
  21. package/dist/gateway/lattice-invoke.js +245 -0
  22. package/dist/gateway/lattice-upstream.d.ts +38 -0
  23. package/dist/gateway/lattice-upstream.js +136 -0
  24. package/dist/gateway/server.js +2 -3
  25. package/dist/gateway/session.d.ts +3 -1
  26. package/dist/gateway/session.js +10 -3
  27. package/dist/manifest-next.d.ts +55 -0
  28. package/dist/manifest-next.js +35 -2
  29. package/dist/manifest-static.d.ts +35 -0
  30. package/dist/manifest-static.js +12 -0
  31. package/dist/manifest.d.ts +48 -0
  32. package/dist/next-build.d.ts +2 -1
  33. package/dist/next-build.js +12 -7
  34. package/dist/offline/cache.d.ts +4 -0
  35. package/dist/offline/cache.js +7 -0
  36. package/dist/offline/connector.d.ts +28 -0
  37. package/dist/offline/connector.js +122 -0
  38. package/dist/offline/crypto.d.ts +5 -0
  39. package/dist/offline/crypto.js +24 -0
  40. package/dist/offline/index.d.ts +8 -0
  41. package/dist/offline/index.js +7 -0
  42. package/dist/offline/indexed-db.d.ts +10 -0
  43. package/dist/offline/indexed-db.js +104 -0
  44. package/dist/offline/management-types.d.ts +25 -0
  45. package/dist/offline/management-types.js +1 -0
  46. package/dist/offline/outbox.d.ts +7 -0
  47. package/dist/offline/outbox.js +46 -0
  48. package/dist/offline/replay.d.ts +25 -0
  49. package/dist/offline/replay.js +192 -0
  50. package/dist/offline/secure-store.d.ts +6 -0
  51. package/dist/offline/secure-store.js +51 -0
  52. package/dist/offline/types.d.ts +162 -0
  53. package/dist/offline/types.js +1 -0
  54. package/dist/orm-json-scalars.d.ts +21 -0
  55. package/dist/orm-json-scalars.js +21 -0
  56. package/dist/runtime-contracts.d.ts +7 -7
  57. package/dist/runtime-contracts.js +1 -1
  58. package/dist/sdk-orm.d.ts +7 -1
  59. package/dist/sdk-orm.js +12 -5
  60. package/dist/sdk.d.ts +134 -1
  61. package/dist/server/api-client.d.ts +26 -0
  62. package/dist/server/api-client.js +38 -0
  63. package/dist/server/broker-contract.d.ts +191 -5
  64. package/dist/server/broker-contract.js +142 -1
  65. package/dist/server/client.d.ts +82 -47
  66. package/dist/server/client.js +138 -59
  67. package/dist/server/connection-retry.d.ts +12 -0
  68. package/dist/server/connection-retry.js +40 -0
  69. package/dist/server/errors.d.ts +61 -0
  70. package/dist/server/errors.js +89 -0
  71. package/dist/server/external-context.d.ts +21 -0
  72. package/dist/server/external-context.js +19 -0
  73. package/dist/server/internal/signed-token.d.ts +15 -0
  74. package/dist/server/internal/signed-token.js +67 -0
  75. package/dist/server/internal/v1-context.d.ts +18 -0
  76. package/dist/server/internal/v1-context.js +26 -0
  77. package/dist/server/internal/verify-any.d.ts +22 -0
  78. package/dist/server/internal/verify-any.js +53 -0
  79. package/dist/server/logger.d.ts +1 -1
  80. package/dist/server/managed-server-path.d.ts +6 -0
  81. package/dist/server/managed-server-path.js +14 -0
  82. package/dist/server/principal.d.ts +48 -0
  83. package/dist/server/principal.js +39 -0
  84. package/dist/server/processes.d.ts +290 -0
  85. package/dist/server/processes.js +56 -0
  86. package/dist/server/request-context.d.ts +121 -3
  87. package/dist/server/request-context.js +111 -33
  88. package/dist/server/testing.d.ts +40 -0
  89. package/dist/server/testing.js +86 -0
  90. package/dist/server.d.ts +13 -4
  91. package/dist/server.js +6 -2
  92. package/dist/source-resolver.d.ts +9 -2
  93. package/dist/source-resolver.js +55 -8
  94. package/dist/templates/nextjs/AGENTS.md +10 -0
  95. package/dist/templates/nextjs/CLAUDE.md +1 -0
  96. package/dist/templates/nextjs/README.md +88 -0
  97. package/dist/templates/nextjs/package.json +3 -1
  98. package/dist/templates/nextjs/tests/server.test.mjs +25 -0
  99. package/dist/templates/react-vite/AGENTS.md +8 -88
  100. package/dist/templates/react-vite/CLAUDE.md +0 -6
  101. package/dist/templates/react-vite/package.json +1 -1
  102. package/dist/templates/react-vite/src/App.tsx +37 -14
  103. package/dist/templates/react-vite/src/styles.css +10 -0
  104. package/package.json +15 -3
  105. package/templates/nextjs/AGENTS.md +10 -0
  106. package/templates/nextjs/CLAUDE.md +1 -0
  107. package/templates/nextjs/README.md +88 -0
  108. package/templates/nextjs/package.json +3 -1
  109. package/templates/nextjs/tests/server.test.mjs +25 -0
  110. package/templates/react-vite/AGENTS.md +8 -88
  111. package/templates/react-vite/CLAUDE.md +0 -6
  112. package/templates/react-vite/package.json +1 -1
  113. package/templates/react-vite/src/App.tsx +37 -14
  114. package/templates/react-vite/src/styles.css +10 -0
@@ -0,0 +1,290 @@
1
+ import { type ArtifactProcessPrincipalType, type ArtifactProcessRunStatus, type ArtifactProcessTaskView, type ArtifactServerMode, type ArtifactServerRequest } from './broker-contract.js';
2
+ /** A run of a process declared under `server.capabilities.processes.start`. */
3
+ export interface ArtifactProcessStart {
4
+ process: string;
5
+ /** Idempotency key: starting the same run id again returns the existing run. */
6
+ runId?: string;
7
+ title?: string;
8
+ entity?: {
9
+ type: string;
10
+ id: string;
11
+ };
12
+ /** The run's input, readable by the process as `ctx.run.metadata`. */
13
+ metadata?: Record<string, unknown>;
14
+ /**
15
+ * Who works each human node of this run, keyed by node id (`execute`) or by a path into a
16
+ * nested subprocess (`application::qa_review`): the node's advancers when the run reaches it,
17
+ * over the process's own defaults. Users are Auth0 subjects, groups are chat group ids; Atlas
18
+ * refuses principals it has tombstoned. Reassign a parked step later with `assign`.
19
+ */
20
+ assignments?: Record<string, ArtifactProcessPrincipals>;
21
+ }
22
+ export interface ArtifactProcessRun {
23
+ run_id: string;
24
+ process_id: string;
25
+ process_version: string;
26
+ status: 'pending' | 'existing';
27
+ idempotent_replay: boolean;
28
+ /**
29
+ * created: a new run was dispatched · replayed: an undispatched run with this id was dispatched ·
30
+ * existing: the run already ran or is running, so nothing was dispatched · unclaimed: a run with
31
+ * this id exists and was not taken over · queued: the process is at its concurrency limit; the run
32
+ * is `pending` and starts when a slot frees.
33
+ */
34
+ outcome: 'created' | 'replayed' | 'existing' | 'unclaimed' | 'queued';
35
+ }
36
+ /** Which runs of a process declared under `server.capabilities.processes.read` to list. */
37
+ export interface ArtifactProcessRunsQuery {
38
+ process: string;
39
+ status?: ArtifactProcessRunStatus;
40
+ /** Runs about one entity type, or one entity. */
41
+ entity?: {
42
+ type: string;
43
+ id?: string;
44
+ };
45
+ /** Newest first; Lattice caps a page at ARTIFACT_PROCESS_RUNS_LIMIT and defaults to 50. */
46
+ limit?: number;
47
+ /** Soft-archived runs only, instead of the default active ones. */
48
+ archived?: boolean;
49
+ }
50
+ /**
51
+ * The stable fields of a Lattice run row as the read API serves it (Atlas may serve more).
52
+ * `metadata` (the run's input) is redacted on read for every viewer, so an application keeps the
53
+ * data it needs back in its own ORM namespace. Timestamps are ISO strings.
54
+ */
55
+ export interface ArtifactProcessRunRecord {
56
+ id: string;
57
+ processId: string;
58
+ processVersion: string;
59
+ status: ArtifactProcessRunStatus;
60
+ title: string | null;
61
+ entityType: string | null;
62
+ entityId: string | null;
63
+ startNodeId: string | null;
64
+ startedBy: string | null;
65
+ startedAt: string;
66
+ completedAt: string | null;
67
+ archivedAt: string | null;
68
+ error: string | null;
69
+ output: unknown;
70
+ /** Node-produced run state; its access boundary is the run's Viewer relation. */
71
+ state: Record<string, unknown>;
72
+ }
73
+ /** The stable fields of one step attempt of a run. Wait-token credentials and runner plumbing are redacted. */
74
+ export interface ArtifactProcessStepRecord {
75
+ id: string;
76
+ runId: string;
77
+ nodeId: string;
78
+ kind: string;
79
+ attempt: number;
80
+ status: string;
81
+ startedAt: string;
82
+ completedAt: string | null;
83
+ input: unknown;
84
+ output: unknown;
85
+ error: string | null;
86
+ nextEdgeId: string | null;
87
+ waitTokenId: string | null;
88
+ }
89
+ /** The collapsed lifecycle of one node: its latest attempt with finished-branch events applied. */
90
+ export type ArtifactProcessNodeState = 'pending' | 'running' | 'completed' | 'failed' | 'skipped' | 'cancelled' | 'timed_out';
91
+ /** One run as `GET /api/lattice/runs/[runId]` serves it. */
92
+ export interface ArtifactProcessRunSnapshot {
93
+ run: ArtifactProcessRunRecord;
94
+ /** Raw step rows, one per attempt / fan-out branch. */
95
+ steps: ArtifactProcessStepRecord[];
96
+ states: Record<string, ArtifactProcessNodeState>;
97
+ /** Pass to the run's event stream as `?since=` to receive everything after this snapshot. */
98
+ event_cursor: string | null;
99
+ }
100
+ /** Which applied version of a process declared under `processes.read` to describe. */
101
+ export interface ArtifactProcessDescribeQuery {
102
+ process: string;
103
+ /** A run's `processVersion`, to see the template that run was started from. Default: the active version. */
104
+ version?: string;
105
+ }
106
+ export interface ArtifactProcessEdge {
107
+ id: string;
108
+ /** The node this edge routes to. */
109
+ to: string;
110
+ }
111
+ /** A static Artifact Studio (or uri) reference a human node renders as its work surface. */
112
+ export interface ArtifactProcessArtifactRef {
113
+ provider: string;
114
+ artifact_id: string;
115
+ project: string | null;
116
+ role: string | null;
117
+ uri: string | null;
118
+ metadata: Record<string, unknown>;
119
+ }
120
+ export interface ArtifactProcessNodeBase {
121
+ id: string;
122
+ kind: 'automation' | 'agent' | 'human' | 'parallel' | 'subprocess' | 'delay' | 'managed_function' | 'managed_server' | (string & {});
123
+ title: string;
124
+ description: string | null;
125
+ outgoing_edges: ArtifactProcessEdge[];
126
+ }
127
+ /** A human node as the people who work it see it; the engine's own node configuration is not part of this. */
128
+ export interface ArtifactProcessHumanNode extends ArtifactProcessNodeBase {
129
+ kind: 'human';
130
+ /** The manual advance choices: `outgoing_edges` less any dedicated timeout route. */
131
+ advance_edges: ArtifactProcessEdge[];
132
+ /** The author's label for the single completion action, or null for the surface's default. */
133
+ complete_label: string | null;
134
+ /** The author's app-facing metadata (`metadata` on `defineHumanNode`): surface hints, form fields, … */
135
+ metadata: Record<string, unknown>;
136
+ comments_enabled: boolean;
137
+ /** Static refs only; refs a mapper resolves per run are not listed. */
138
+ artifact_refs: ArtifactProcessArtifactRef[];
139
+ }
140
+ export type ArtifactProcessNode = ArtifactProcessNodeBase | ArtifactProcessHumanNode;
141
+ /**
142
+ * One applied version of a process: the template its runs are filled in from. Automation, agent,
143
+ * managed-function, subprocess and parallel internals are the process's own and are never served here.
144
+ */
145
+ export interface ArtifactProcessDefinition {
146
+ id: string;
147
+ name: string;
148
+ description: string | null;
149
+ version: string;
150
+ bundle_hash: string;
151
+ /** Process-level switch: `processes.start` is refused while set. */
152
+ new_runs_disabled: boolean;
153
+ start_node_id: string;
154
+ /** JSON Schema for the `metadata` a start accepts, when the author declared one. */
155
+ input_schema: Record<string, unknown> | null;
156
+ tags: string[];
157
+ /** Every node, flattened: a human node inside a parallel branch is listed beside the top-level nodes. */
158
+ nodes: ArtifactProcessNode[];
159
+ }
160
+ /** Which of the user's human steps to list, across processes declared under `processes.read`. */
161
+ export interface ArtifactProcessTasksQuery {
162
+ /** A subset of the declared processes; default every declared one. */
163
+ processes?: string[];
164
+ /** `active` (default): parked and waiting on the user. `completed`: steps they were assigned that are done. */
165
+ view?: ArtifactProcessTaskView;
166
+ /** Newest first; Lattice caps a page at ARTIFACT_PROCESS_TASKS_LIMIT and defaults to 100. */
167
+ limit?: number;
168
+ }
169
+ /** One human step of the user's, with what the node's template says about working it. Timestamps are ISO strings. */
170
+ export interface ArtifactProcessTask {
171
+ step_id: string;
172
+ run_id: string;
173
+ node_id: string;
174
+ process_id: string;
175
+ process_version: string;
176
+ process_name: string | null;
177
+ title: string;
178
+ description: string | null;
179
+ entity: {
180
+ type: string;
181
+ id: string;
182
+ } | null;
183
+ run_title: string | null;
184
+ advance_edges: ArtifactProcessEdge[];
185
+ complete_label: string | null;
186
+ metadata: Record<string, unknown>;
187
+ /** How the step reached the user: assigned to them, to one of their groups, or reassigned by them. */
188
+ assigned_via: 'user' | 'group' | 'reassigned';
189
+ started_at: string;
190
+ due_at: string | null;
191
+ /** Completed view only. */
192
+ completed_at?: string | null;
193
+ /** Completed view only: `completed`, or `timed_out` for a step that expired instead of being worked. */
194
+ step_status?: string;
195
+ /** Completed view only: the run's status at read time. */
196
+ run_status?: string;
197
+ /** The node's template could not be loaded; identity fields are still exact. */
198
+ template_unavailable?: boolean;
199
+ }
200
+ /** Completing one of the user's parked human steps on a run of a process declared under `processes.act`. */
201
+ export interface ArtifactProcessAdvance {
202
+ runId: string;
203
+ /** Which parked step; required when a parallel block has several human branches parked at once. */
204
+ stepId?: string;
205
+ /** One of the node's `advance_edges`; omitted on a terminal human node. */
206
+ edgeId?: string;
207
+ /** The step's output: the node's form fields, as the process reads them. */
208
+ payload?: Record<string, unknown>;
209
+ }
210
+ export interface ArtifactProcessAdvanceResult {
211
+ step_id: string;
212
+ node_id: string;
213
+ edge_id: string | null;
214
+ /** The step had already been completed with this decision; nothing was re-run. */
215
+ idempotent_replay: boolean;
216
+ }
217
+ /** Users (Auth0 `sub`) and chat groups (group id) to name in an assignment or a share. */
218
+ export interface ArtifactProcessPrincipals {
219
+ users?: string[];
220
+ groups?: string[];
221
+ }
222
+ /** Reassigning a parked human step: the step's advancers become exactly these principals. */
223
+ export interface ArtifactProcessAssign extends ArtifactProcessPrincipals {
224
+ runId: string;
225
+ stepId: string;
226
+ }
227
+ export interface ArtifactProcessAssignResult {
228
+ step_id: string;
229
+ node_id: string;
230
+ assigned: {
231
+ users: string[];
232
+ groups: string[];
233
+ };
234
+ }
235
+ /** The explicit `viewer` grants stored on a run; access derived from the process or a step is not listed. */
236
+ export interface ArtifactProcessRunViewers {
237
+ run_id: string;
238
+ users: string[];
239
+ groups: string[];
240
+ }
241
+ export interface ArtifactProcessRunViewerChange extends ArtifactProcessPrincipals {
242
+ runId: string;
243
+ }
244
+ export interface ArtifactProcessPrincipalSearch {
245
+ /** Empty browses the first `limit` users; 1–2 characters match groups only; 3+ search the directory. */
246
+ q: string;
247
+ /** Default both. */
248
+ types?: ArtifactProcessPrincipalType[];
249
+ /** Users and groups together; Lattice caps at ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, default 20. */
250
+ limit?: number;
251
+ }
252
+ /** A principal the user may name. `id` is the value `assign` and the viewer grants consume. */
253
+ export interface ArtifactProcessPrincipal {
254
+ type: ArtifactProcessPrincipalType;
255
+ id: string;
256
+ name: string;
257
+ email?: string;
258
+ picture?: string;
259
+ /** Groups: active members. */
260
+ memberCount?: number;
261
+ }
262
+ export interface ArtifactServerProcesses {
263
+ /** Start a run of a process declared under `processes.start`; the user's Invoker grant decides. */
264
+ start(input: ArtifactProcessStart): Promise<ArtifactProcessRun>;
265
+ /** List the runs of a process declared under `processes.read` that the user may view. */
266
+ listRuns(query: ArtifactProcessRunsQuery): Promise<ArtifactProcessRunRecord[]>;
267
+ /** Read one run of a process declared under `processes.read`; the user's Viewer relation on the run decides. */
268
+ getRun(runId: string): Promise<ArtifactProcessRunSnapshot>;
269
+ /** Describe a process declared under `processes.read`; the user's Viewer relation on the process decides. */
270
+ describe(query: ArtifactProcessDescribeQuery): Promise<ArtifactProcessDefinition>;
271
+ /** The user's own human steps across processes declared under `processes.read`. */
272
+ listTasks(query?: ArtifactProcessTasksQuery): Promise<ArtifactProcessTask[]>;
273
+ /** Complete a parked human step on a run of a process declared under `processes.act`; the user's own `advancer` grant decides. */
274
+ advance(input: ArtifactProcessAdvance): Promise<ArtifactProcessAdvanceResult>;
275
+ /** Reassign a parked human step; the user's `owner` relation on the run decides. */
276
+ assign(input: ArtifactProcessAssign): Promise<ArtifactProcessAssignResult>;
277
+ /** The explicit viewer grants on a run of a process declared under `processes.act`; owner-gated. */
278
+ listViewers(runId: string): Promise<ArtifactProcessRunViewers>;
279
+ /** Share a run with users and groups; owner-gated, additive, idempotent. */
280
+ grantViewers(input: ArtifactProcessRunViewerChange): Promise<ArtifactProcessRunViewers>;
281
+ /** Take a share back; owner-gated, idempotent. Returns the grants that remain. */
282
+ revokeViewers(input: ArtifactProcessRunViewerChange): Promise<ArtifactProcessRunViewers>;
283
+ /** Users in the user's own tenant and groups they may see, to name in `assign` or a share. */
284
+ searchPrincipals(query: ArtifactProcessPrincipalSearch): Promise<ArtifactProcessPrincipal[]>;
285
+ }
286
+ /** `client.processes`: Lattice through the broker's typed `lattice*` operations. Atlas owns validation and redaction. */
287
+ export declare function createArtifactProcessesClient({ mode, json }: {
288
+ mode: ArtifactServerMode;
289
+ json: (operation: ArtifactServerRequest) => Promise<unknown>;
290
+ }): ArtifactServerProcesses;
@@ -0,0 +1,56 @@
1
+ import { artifactServerRequestSchema, } from './broker-contract.js';
2
+ import { ArtifactServerError } from './errors.js';
3
+ /** `client.processes`: Lattice through the broker's typed `lattice*` operations. Atlas owns validation and redaction. */
4
+ export function createArtifactProcessesClient({ mode, json }) {
5
+ return {
6
+ start: async ({ process, ...run }) => {
7
+ // Atlas returns the started run's identifiers; the shape is the Lattice start response.
8
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice', mode, process, ...run }));
9
+ },
10
+ listRuns: async (query) => {
11
+ // The Lattice list response: `{ runs }`, already filtered to what the user may view.
12
+ const body = await json(artifactServerRequestSchema.parse({ operation: 'lattice_runs', mode, ...query }));
13
+ if (!Array.isArray(body?.runs))
14
+ throw new ArtifactServerError(502, 'malformed_response', 'Atlas returned a malformed run list');
15
+ return body.runs;
16
+ },
17
+ getRun: async (runId) => {
18
+ // The Lattice run-detail response; Atlas owns redaction and node-state derivation.
19
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_run', mode, runId }));
20
+ },
21
+ describe: async (query) => {
22
+ // Atlas projects the applied bundle; engine internals never leave it.
23
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_process', mode, ...query }));
24
+ },
25
+ listTasks: async (query = {}) => {
26
+ // The Lattice inbox response shape: `{ items }`, already the user's own.
27
+ const body = await json(artifactServerRequestSchema.parse({ operation: 'lattice_tasks', mode, ...query }));
28
+ if (!Array.isArray(body?.items))
29
+ throw new ArtifactServerError(502, 'malformed_response', 'Atlas returned a malformed task list');
30
+ return body.items;
31
+ },
32
+ advance: async (input) => {
33
+ // The Lattice advance response; Atlas owns the step resolution, the ACL and the walk.
34
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_advance', mode, ...input }));
35
+ },
36
+ assign: async (input) => {
37
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_assign', mode, ...input }));
38
+ },
39
+ listViewers: async (runId) => {
40
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_viewers', mode, runId }));
41
+ },
42
+ grantViewers: async (input) => {
43
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_viewer_grant', mode, ...input }));
44
+ },
45
+ revokeViewers: async (input) => {
46
+ return await json(artifactServerRequestSchema.parse({ operation: 'lattice_viewer_revoke', mode, ...input }));
47
+ },
48
+ searchPrincipals: async (query) => {
49
+ // The Lattice picker response shape: `{ principals }`, already scoped to the user's tenant.
50
+ const body = await json(artifactServerRequestSchema.parse({ operation: 'lattice_principals', mode, ...query }));
51
+ if (!Array.isArray(body?.principals))
52
+ throw new ArtifactServerError(502, 'malformed_response', 'Atlas returned a malformed principal list');
53
+ return body.principals;
54
+ },
55
+ };
56
+ }
@@ -1,9 +1,16 @@
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";
6
7
  export declare const ARTIFACT_CONTEXT_MAX_AGE_SECONDS = 60;
8
+ /**
9
+ * Lattice managed-server invocations last up to the node timeout (300s). A
10
+ * browser context stays at {@link ARTIFACT_CONTEXT_MAX_AGE_SECONDS}. The longer
11
+ * window applies only when the signed context names a Lattice run.
12
+ */
13
+ export declare const ARTIFACT_LATTICE_CONTEXT_MAX_AGE_SECONDS = 300;
7
14
  /** Public request context. No bearer credentials or secret values belong here. */
8
15
  export declare const artifactRequestContextSchema: z.ZodObject<{
9
16
  v: z.ZodLiteral<1>;
@@ -55,10 +62,121 @@ export declare const artifactRequestContextSchema: z.ZodObject<{
55
62
  }, z.core.$strict>;
56
63
  export type ArtifactRequestContext = z.infer<typeof artifactRequestContextSchema>;
57
64
  export type ArtifactGatewayPublicKeys = Readonly<Record<string, string>>;
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 declare const ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX = "service-account|";
71
+ /** An external context's lifetime cap. External contexts never carry a Lattice run. */
72
+ export declare const ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS = 60;
73
+ /** The external caller as the gateway verified it at pass exchange. Strict: see the v2 key policy below. */
74
+ export declare const artifactExternalPrincipalSchema: z.ZodObject<{
75
+ kind: z.ZodLiteral<"external">;
76
+ ingress: z.ZodString;
77
+ provider: z.ZodString;
78
+ issuer: z.ZodString;
79
+ subject: z.ZodString;
80
+ clientId: z.ZodString;
81
+ email: z.ZodString;
82
+ emailVerified: z.ZodLiteral<true>;
83
+ authTime: z.ZodNullable<z.ZodNumber>;
84
+ amr: z.ZodNullable<z.ZodArray<z.ZodString>>;
85
+ tokenIssuedAt: z.ZodNumber;
86
+ pass: z.ZodObject<{
87
+ id: z.ZodUUID;
88
+ kid: z.ZodString;
89
+ issuedAt: z.ZodNumber;
90
+ exchangeIp: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
91
+ }, z.core.$strict>;
92
+ client: z.ZodObject<{
93
+ ip: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
94
+ userAgent: z.ZodString;
95
+ userAgentSha256: z.ZodString;
96
+ origin: z.ZodNullable<z.ZodString>;
97
+ }, z.core.$strict>;
98
+ }, z.core.$strict>;
99
+ /**
100
+ * External request context (v2): a caller that is not an Atlas user, admitted through a
101
+ * manifest-declared ingress. Key policy: any unknown top-level or `principal` key is refused
102
+ * unless it sits under `ext`. `crit` is reserved for extensions a verifier must understand; this
103
+ * verifier understands none, so it must be empty. Security-relevant additions bump `v`.
104
+ *
105
+ * Public verifiers ({@link verifyArtifactRequestContext}) never accept this version; only the
106
+ * explicit external APIs do.
107
+ */
108
+ export declare const requestContextV2ExternalSchema: z.ZodObject<{
109
+ v: z.ZodLiteral<2>;
110
+ iss: z.ZodLiteral<"atlas-artifact-gateway">;
111
+ aud: z.ZodString;
112
+ sub: z.ZodString;
113
+ projectId: z.ZodString;
114
+ environment: z.ZodString;
115
+ mode: z.ZodEnum<{
116
+ published: "published";
117
+ development: "development";
118
+ }>;
119
+ deploymentId: z.ZodNullable<z.ZodString>;
120
+ devSessionId: z.ZodNullable<z.ZodString>;
121
+ requestId: z.ZodString;
122
+ jti: z.ZodUUID;
123
+ method: z.ZodEnum<{
124
+ GET: "GET";
125
+ POST: "POST";
126
+ DELETE: "DELETE";
127
+ HEAD: "HEAD";
128
+ PUT: "PUT";
129
+ PATCH: "PATCH";
130
+ OPTIONS: "OPTIONS";
131
+ }>;
132
+ path: z.ZodString;
133
+ iat: z.ZodNumber;
134
+ exp: z.ZodNumber;
135
+ serviceAccountId: z.ZodString;
136
+ principal: z.ZodObject<{
137
+ kind: z.ZodLiteral<"external">;
138
+ ingress: z.ZodString;
139
+ provider: z.ZodString;
140
+ issuer: z.ZodString;
141
+ subject: z.ZodString;
142
+ clientId: z.ZodString;
143
+ email: z.ZodString;
144
+ emailVerified: z.ZodLiteral<true>;
145
+ authTime: z.ZodNullable<z.ZodNumber>;
146
+ amr: z.ZodNullable<z.ZodArray<z.ZodString>>;
147
+ tokenIssuedAt: z.ZodNumber;
148
+ pass: z.ZodObject<{
149
+ id: z.ZodUUID;
150
+ kid: z.ZodString;
151
+ issuedAt: z.ZodNumber;
152
+ exchangeIp: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
153
+ }, z.core.$strict>;
154
+ client: z.ZodObject<{
155
+ ip: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
156
+ userAgent: z.ZodString;
157
+ userAgentSha256: z.ZodString;
158
+ origin: z.ZodNullable<z.ZodString>;
159
+ }, z.core.$strict>;
160
+ }, z.core.$strict>;
161
+ crit: z.ZodArray<z.ZodString>;
162
+ ext: z.ZodRecord<z.ZodString, z.ZodUnknown>;
163
+ }, z.core.$strict>;
164
+ export type RequestContextV2External = z.infer<typeof requestContextV2ExternalSchema>;
165
+ /** Why a context was refused, when the refusal has a name a handler can act on. */
166
+ export type ArtifactContextErrorReason = 'external_context_not_accepted' | 'external_context_required' | 'external_ingress_not_accepted' | 'external_mode_not_accepted';
58
167
  export declare class ArtifactContextError extends Error {
59
- constructor();
168
+ readonly reason: ArtifactContextErrorReason | undefined;
169
+ constructor(reason?: ArtifactContextErrorReason);
60
170
  }
61
- /** Only the gateway owns private keys. Applications pin platform-provided public keys. */
171
+ /**
172
+ * Only the gateway owns private keys. Applications pin platform-provided public keys.
173
+ *
174
+ * Accepts v1 (Atlas user and platform service) contexts only. An external (v2) context, or any
175
+ * token signed with an `external-` kid, is refused with reason `external_context_not_accepted`
176
+ * before any other check, so a borrower context can never reach a handler that reads `sub` and
177
+ * `capabilities` as an Atlas user. External contexts are verified only through
178
+ * `verifyArtifactExternalContext` or `createArtifactServerClient({ accept })`.
179
+ */
62
180
  export declare function verifyArtifactRequestContext({ token, publicKeys, projectId, environment, method, path, now }: {
63
181
  token: string | null;
64
182
  publicKeys: ArtifactGatewayPublicKeys;
@@ -70,6 +188,6 @@ export declare function verifyArtifactRequestContext({ token, publicKeys, projec
70
188
  now?: number;
71
189
  }): ArtifactRequestContext;
72
190
  /** Safe props for the client bridge; intentionally excludes actor and runtime credentials. */
73
- export declare function getArtifactBrowserBootstrap(context: ArtifactRequestContext): NextArtifactBridgeBootstrap & {
191
+ export declare function getArtifactBrowserBootstrap(context: ArtifactRequestContext | RequestContextV2External): NextArtifactBridgeBootstrap & {
74
192
  initialContext: NextArtifactInitialContext;
75
193
  };
@@ -1,10 +1,17 @@
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';
7
8
  export const ARTIFACT_CONTEXT_MAX_AGE_SECONDS = 60;
9
+ /**
10
+ * Lattice managed-server invocations last up to the node timeout (300s). A
11
+ * browser context stays at {@link ARTIFACT_CONTEXT_MAX_AGE_SECONDS}. The longer
12
+ * window applies only when the signed context names a Lattice run.
13
+ */
14
+ export const ARTIFACT_LATTICE_CONTEXT_MAX_AGE_SECONDS = 300;
8
15
  const id = z.string().min(1).max(255);
9
16
  const originSchema = z.string().max(2048).refine(value => {
10
17
  try {
@@ -46,54 +53,125 @@ export const artifactRequestContextSchema = z.object({
46
53
  }).strict().superRefine((value, ctx) => {
47
54
  if (value.aud !== `artifact:${value.projectId}`)
48
55
  ctx.addIssue({ code: 'custom', message: 'Invalid Artifact audience' });
49
- if (value.exp <= value.iat || value.exp - value.iat > ARTIFACT_CONTEXT_MAX_AGE_SECONDS)
56
+ const maxAge = value.lattice.runId !== null
57
+ ? ARTIFACT_LATTICE_CONTEXT_MAX_AGE_SECONDS
58
+ : ARTIFACT_CONTEXT_MAX_AGE_SECONDS;
59
+ if (value.exp <= value.iat || value.exp - value.iat > maxAge)
60
+ ctx.addIssue({ code: 'custom', message: 'Invalid Artifact context lifetime' });
61
+ if (value.mode === 'development' ? !value.devSessionId || value.deploymentId !== null : !value.deploymentId || value.devSessionId !== null)
62
+ ctx.addIssue({ code: 'custom', message: 'Artifact runtime identity does not match its mode' });
63
+ });
64
+ export { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX };
65
+ /**
66
+ * Prefix of a platform service account's `sub`. Mirrors Atlas's `SERVICE_ACCOUNT_SUB_PREFIX`
67
+ * (atlas/src/server/lib/auth0-sub.ts); Atlas's auth0-sub test pins the two together.
68
+ */
69
+ export const ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX = 'service-account|';
70
+ /** An external context's lifetime cap. External contexts never carry a Lattice run. */
71
+ export const ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS = 60;
72
+ const ipAddress = z.union([z.ipv4(), z.ipv6()]);
73
+ /** Signed by the gateway after it strips control characters and truncates; refused here otherwise. */
74
+ const observedUserAgent = z.string().max(512).refine(value => !/[\u0000-\u001f\u007f]/.test(value), 'Expected a user agent without control characters');
75
+ /** The external caller as the gateway verified it at pass exchange. Strict: see the v2 key policy below. */
76
+ export const artifactExternalPrincipalSchema = z.object({
77
+ kind: z.literal('external'),
78
+ /** Routing label for the manifest's ingress declaration. Not an authorization attribute. */
79
+ ingress: id,
80
+ provider: id,
81
+ issuer: z.string().min(1).max(2048),
82
+ subject: id,
83
+ clientId: id,
84
+ /** Normalized (trimmed, lowercase) by Atlas at exchange. */
85
+ email: z.string().min(3).max(320).refine(value => value === value.trim().toLowerCase() && value.includes('@'), 'Expected a normalized email'),
86
+ emailVerified: z.literal(true),
87
+ authTime: z.number().int().nonnegative().nullable(),
88
+ amr: z.array(z.string().min(1).max(64)).max(16).nullable(),
89
+ tokenIssuedAt: z.number().int().nonnegative(),
90
+ pass: z.object({ id: z.uuid(), kid: id, issuedAt: z.number().int().nonnegative(), exchangeIp: ipAddress }).strict(),
91
+ client: z.object({
92
+ ip: ipAddress,
93
+ userAgent: observedUserAgent,
94
+ userAgentSha256: z.string().regex(/^[0-9a-f]{64}$/),
95
+ origin: originSchema.nullable(),
96
+ }).strict(),
97
+ }).strict();
98
+ /**
99
+ * External request context (v2): a caller that is not an Atlas user, admitted through a
100
+ * manifest-declared ingress. Key policy: any unknown top-level or `principal` key is refused
101
+ * unless it sits under `ext`. `crit` is reserved for extensions a verifier must understand; this
102
+ * verifier understands none, so it must be empty. Security-relevant additions bump `v`.
103
+ *
104
+ * Public verifiers ({@link verifyArtifactRequestContext}) never accept this version; only the
105
+ * explicit external APIs do.
106
+ */
107
+ export const requestContextV2ExternalSchema = z.object({
108
+ v: z.literal(2),
109
+ iss: z.literal('atlas-artifact-gateway'),
110
+ aud: z.string().min(1).max(300),
111
+ sub: id.refine(value => value.startsWith('ext|'), 'External subjects are namespaced ext|<provider>|<subject>'),
112
+ projectId: id,
113
+ environment: id,
114
+ mode: z.enum(['published', 'development']),
115
+ deploymentId: id.nullable(),
116
+ devSessionId: id.nullable(),
117
+ requestId: id,
118
+ jti: z.uuid(),
119
+ method: artifactRequestContextSchema.shape.method,
120
+ path: artifactRequestContextSchema.shape.path,
121
+ iat: z.number().int().nonnegative(),
122
+ exp: z.number().int().positive(),
123
+ /** The ingress's own service account; external principals act only through service mode. */
124
+ serviceAccountId: id,
125
+ principal: artifactExternalPrincipalSchema,
126
+ crit: z.array(z.string()).length(0),
127
+ ext: z.record(z.string().min(1).max(64), z.unknown()),
128
+ }).strict().superRefine((value, ctx) => {
129
+ if (value.aud !== `artifact:${value.projectId}`)
130
+ ctx.addIssue({ code: 'custom', message: 'Invalid Artifact audience' });
131
+ if (value.sub !== `ext|${value.principal.provider}|${value.principal.subject}`)
132
+ ctx.addIssue({ code: 'custom', message: 'External subject does not match its principal' });
133
+ if (value.exp <= value.iat || value.exp - value.iat > ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS)
50
134
  ctx.addIssue({ code: 'custom', message: 'Invalid Artifact context lifetime' });
51
135
  if (value.mode === 'development' ? !value.devSessionId || value.deploymentId !== null : !value.deploymentId || value.devSessionId !== null)
52
136
  ctx.addIssue({ code: 'custom', message: 'Artifact runtime identity does not match its mode' });
53
137
  });
54
138
  export class ArtifactContextError extends Error {
55
- constructor() { super('Missing or invalid Artifact gateway request context'); this.name = 'ArtifactContextError'; }
56
- }
57
- function decode(value) {
58
- if (!/^[A-Za-z0-9_-]+$/.test(value))
59
- throw new ArtifactContextError();
60
- return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
139
+ reason;
140
+ constructor(reason) {
141
+ super('Missing or invalid Artifact gateway request context');
142
+ this.name = 'ArtifactContextError';
143
+ this.reason = reason;
144
+ }
61
145
  }
62
- /** Only the gateway owns private keys. Applications pin platform-provided public keys. */
146
+ /**
147
+ * Only the gateway owns private keys. Applications pin platform-provided public keys.
148
+ *
149
+ * Accepts v1 (Atlas user and platform service) contexts only. An external (v2) context, or any
150
+ * token signed with an `external-` kid, is refused with reason `external_context_not_accepted`
151
+ * before any other check, so a borrower context can never reach a handler that reads `sub` and
152
+ * `capabilities` as an Atlas user. External contexts are verified only through
153
+ * `verifyArtifactExternalContext` or `createArtifactServerClient({ accept })`.
154
+ */
63
155
  export function verifyArtifactRequestContext({ token, publicKeys, projectId, environment, method, path, now = Math.floor(Date.now() / 1000) }) {
156
+ if (isExternalContextToken(token))
157
+ throw new ArtifactContextError('external_context_not_accepted');
158
+ let payload;
64
159
  try {
65
- if (!token || token.length > 32768)
66
- throw new ArtifactContextError();
67
- const parts = token.split('.');
68
- if (parts.length !== 3)
69
- throw new ArtifactContextError();
70
- const [encodedHeader, encodedPayload, signature] = parts;
71
- const header = z.object({ alg: z.literal('EdDSA'), typ: z.literal('JWT'), kid: id }).strict().parse(decode(encodedHeader));
72
- const pem = Object.hasOwn(publicKeys, header.kid) ? publicKeys[header.kid] : undefined;
73
- if (!pem || !/^[A-Za-z0-9_-]+$/.test(signature))
74
- throw new ArtifactContextError();
75
- const key = createPublicKey(pem);
76
- if (key.asymmetricKeyType !== 'ed25519' ||
77
- !verify(null, Buffer.from(`${encodedHeader}.${encodedPayload}`), key, Buffer.from(signature, 'base64url')))
78
- throw new ArtifactContextError();
79
- const context = artifactRequestContextSchema.parse(decode(encodedPayload));
80
- if (context.projectId !== projectId || context.environment !== environment ||
81
- context.exp <= now || context.iat > now + 5 ||
82
- (method !== undefined && context.method !== method.toUpperCase()) || (path !== undefined && context.path !== path))
160
+ const verified = verifySignedContext({ token, publicKeys });
161
+ if (verified.kid.startsWith(ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX))
83
162
  throw new ArtifactContextError();
84
- Object.freeze(context.bridge);
85
- Object.freeze(context.lattice);
86
- Object.freeze(context.capabilities);
87
- if (context.taskRoutes)
88
- Object.freeze(context.taskRoutes);
89
- return Object.freeze(context);
163
+ payload = verified.payload;
90
164
  }
91
165
  catch {
92
166
  throw new ArtifactContextError();
93
167
  }
168
+ return parseV1Context({ payload, projectId, environment, method, path, now });
94
169
  }
95
170
  /** Safe props for the client bridge; intentionally excludes actor and runtime credentials. */
96
171
  export function getArtifactBrowserBootstrap(context) {
172
+ // An external caller has no Atlas bridge, theme or Lattice run to hand to a browser.
173
+ if (context.v !== 1)
174
+ throw new ArtifactContextError('external_context_not_accepted');
97
175
  return {
98
176
  parentOrigin: context.bridge.parentOrigin,
99
177
  channelId: context.bridge.channelId,