@sequenceholdings/artifact-studio 0.6.4 → 0.6.6
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.
- package/README.md +183 -6
- package/VERSION-PIN.md +56 -1
- package/dist/gateway/admission.d.ts +23 -1
- package/dist/gateway/admission.js +30 -13
- package/dist/gateway/control.d.ts +40 -0
- package/dist/gateway/control.js +76 -7
- package/dist/gateway/hosted-proxy.d.ts +11 -8
- package/dist/gateway/hosted-proxy.js +99 -91
- package/dist/gateway/ingress-admission.d.ts +77 -0
- package/dist/gateway/ingress-admission.js +150 -0
- package/dist/gateway/ingress-audit.d.ts +46 -0
- package/dist/gateway/ingress-audit.js +84 -0
- package/dist/gateway/ingress-context.d.ts +57 -0
- package/dist/gateway/ingress-context.js +57 -0
- package/dist/gateway/ingress-development.d.ts +12 -0
- package/dist/gateway/ingress-development.js +23 -0
- package/dist/gateway/ingress-handler.d.ts +44 -0
- package/dist/gateway/ingress-handler.js +320 -0
- package/dist/gateway/ingress-pass.d.ts +83 -0
- package/dist/gateway/ingress-pass.js +143 -0
- package/dist/gateway/ingress-response.d.ts +74 -0
- package/dist/gateway/ingress-response.js +98 -0
- package/dist/gateway/ingress-runtime.d.ts +86 -0
- package/dist/gateway/ingress-runtime.js +126 -0
- package/dist/gateway/ingress-snapshot.d.ts +54 -0
- package/dist/gateway/ingress-snapshot.js +63 -0
- package/dist/gateway/ingress-stages.d.ts +60 -0
- package/dist/gateway/ingress-stages.js +387 -0
- package/dist/gateway/ingress.d.ts +259 -0
- package/dist/gateway/ingress.js +204 -0
- package/dist/gateway/lattice-invoke.js +2 -0
- package/dist/gateway/lattice-upstream.d.ts +3 -4
- package/dist/gateway/lattice-upstream.js +11 -9
- package/dist/gateway/runtime-transport.d.ts +17 -0
- package/dist/gateway/runtime-transport.js +26 -0
- package/dist/gateway/server.d.ts +3 -0
- package/dist/gateway/server.js +21 -0
- package/dist/gateway/session-response.d.ts +15 -0
- package/dist/gateway/session-response.js +40 -0
- package/dist/gateway/usage.d.ts +8 -1
- package/dist/gateway-main.js +32 -8
- package/dist/manifest-next-ingress.d.ts +5 -2
- package/dist/manifest-next-ingress.js +27 -2
- package/dist/manifest-next.d.ts +20 -6
- package/dist/manifest-next.js +13 -5
- package/dist/manifest.d.ts +7 -2
- package/dist/next-project.d.ts +4 -0
- package/dist/sdk.d.ts +12 -3
- package/dist/server/broker-contract.d.ts +62 -0
- package/dist/server/broker-contract.js +43 -1
- package/dist/server/client.js +38 -13
- package/dist/server/external-context.d.ts +8 -1
- package/dist/server/external-context.js +7 -2
- package/dist/server/internal/v1-context.js +17 -1
- package/dist/server/internal/verify-any.d.ts +11 -2
- package/dist/server/internal/verify-any.js +11 -3
- package/dist/server/process-run-sharing.d.ts +86 -0
- package/dist/server/process-run-sharing.js +29 -0
- package/dist/server/process-runs-page.d.ts +39 -0
- package/dist/server/process-runs-page.js +10 -0
- package/dist/server/processes.d.ts +8 -9
- package/dist/server/processes.js +4 -0
- package/dist/server/request-context.js +10 -3
- package/dist/server/runtime-config.d.ts +10 -1
- package/dist/server/runtime-config.js +11 -1
- package/dist/server/testing.d.ts +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/tunnel/broker.d.ts +18 -0
- package/dist/tunnel/broker.js +175 -17
- package/dist/tunnel/client.d.ts +13 -0
- package/dist/tunnel/client.js +40 -2
- package/dist/tunnel/external.d.ts +51 -0
- package/dist/tunnel/external.js +10 -0
- package/dist/tunnel/index.d.ts +1 -1
- package/dist/tunnel/protocol.d.ts +20 -2
- package/dist/tunnel/protocol.js +42 -6
- package/package.json +4 -4
|
@@ -31,6 +31,12 @@ const latticeVersion = z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/);
|
|
|
31
31
|
export const ARTIFACT_PROCESS_RUN_STATUSES = ['pending', 'running', 'completed', 'failed', 'cancelled', 'timed_out'];
|
|
32
32
|
/** Lattice lists at most this many runs per call. */
|
|
33
33
|
export const ARTIFACT_PROCESS_RUNS_LIMIT = 500;
|
|
34
|
+
/** A run page can filter to at most this many entities of one type. */
|
|
35
|
+
export const ARTIFACT_PROCESS_RUN_ENTITIES_LIMIT = 500;
|
|
36
|
+
/** A run share adds collaborators to at most this many explicitly assigned human nodes. */
|
|
37
|
+
export const ARTIFACT_PROCESS_SHARE_NODES_LIMIT = 100;
|
|
38
|
+
/** An owner may list at most this many sharing authorities per page. */
|
|
39
|
+
export const ARTIFACT_PROCESS_SHARING_AUTHORITIES_LIMIT = 50;
|
|
34
40
|
/** A run-events read returns at most this many events per call, as the run's stream drains them. */
|
|
35
41
|
export const ARTIFACT_PROCESS_RUN_EVENTS_LIMIT = 1000;
|
|
36
42
|
/** Which of the user's human steps to list: parked and waiting on them, or already done. */
|
|
@@ -65,8 +71,14 @@ const assignmentNodeKey = z.string().min(1).max(255).regex(/^[A-Za-z0-9._-]+(?::
|
|
|
65
71
|
const runAssignments = z.record(assignmentNodeKey, z.object(principalSet).strict().refine(namesPrincipals, PRINCIPAL_SET_MESSAGE))
|
|
66
72
|
.refine((value) => Object.keys(value).length > 0 && Object.keys(value).length <= ARTIFACT_PROCESS_START_ASSIGNMENT_NODES, `Between 1 and ${ARTIFACT_PROCESS_START_ASSIGNMENT_NODES} assigned nodes`);
|
|
67
73
|
const entityRef = z.string().min(1).max(255);
|
|
74
|
+
const runPageEntity = z.object({
|
|
75
|
+
type: entityRef, id: entityRef.optional(),
|
|
76
|
+
ids: z.array(entityRef).min(1).max(ARTIFACT_PROCESS_RUN_ENTITIES_LIMIT).optional(),
|
|
77
|
+
}).strict().refine(value => value.id === undefined || value.ids === undefined, 'Name one entity id or a set of ids, not both');
|
|
68
78
|
// Unleash feature names are URL-safe.
|
|
69
79
|
const flagName = z.string().min(1).max(255).regex(/^[A-Za-z0-9._~-]+$/);
|
|
80
|
+
/** The data plane's own idempotency-key rule (`orm-data-plane/src/runtime.ts`), so a key the SDK sends is one it accepts. */
|
|
81
|
+
export const artifactOrmIdempotencyKeySchema = z.string().regex(/^[A-Za-z0-9._:-]{1,128}$/);
|
|
70
82
|
export const ARTIFACT_FLAGS_PER_REQUEST = 32;
|
|
71
83
|
/** The audit actions an application may record. Sign-in, identity merges and webhooks stay Atlas's. */
|
|
72
84
|
export const ARTIFACT_AUDIT_ACTIONS = [
|
|
@@ -97,10 +109,13 @@ const auditEvent = {
|
|
|
97
109
|
export const artifactServerRequestSchema = z.discriminatedUnion('operation', [
|
|
98
110
|
z.object({ operation: z.literal('api'), mode, method, path, body: z.unknown().optional() }).strict(),
|
|
99
111
|
// jsonScalars asks the data plane for exact numeric and bigint strings; Atlas forwards it as
|
|
100
|
-
// the data plane's own header and passes the acknowledgment back.
|
|
112
|
+
// the data plane's own header and passes the acknowledgment back. idempotencyKey names one
|
|
113
|
+
// logical mutation so a retry returns its receipt; Atlas forwards it in the data plane body
|
|
114
|
+
// (re-scoped per caller for external principals) and the data plane refuses it on a query.
|
|
101
115
|
z.object({
|
|
102
116
|
operation: z.literal('orm'), mode, namespace: id, op: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
103
117
|
variables: z.record(z.string(), z.unknown()).optional(), jsonScalars: z.literal(ORM_LOSSLESS_JSON_SCALARS).optional(),
|
|
118
|
+
idempotencyKey: artifactOrmIdempotencyKeySchema.optional(),
|
|
104
119
|
}).strict(),
|
|
105
120
|
z.object({ operation: z.literal('storage'), mode, method, path: path.refine(value => {
|
|
106
121
|
const pathname = value.split('?')[0];
|
|
@@ -128,6 +143,17 @@ export const artifactServerRequestSchema = z.discriminatedUnion('operation', [
|
|
|
128
143
|
limit: z.number().int().min(1).max(ARTIFACT_PROCESS_RUNS_LIMIT).optional(),
|
|
129
144
|
archived: z.boolean().optional(),
|
|
130
145
|
}).strict(),
|
|
146
|
+
// Keyset pagination selects a bounded window from the principal's authorized runs.
|
|
147
|
+
// Continuation is returned only when another visible run exists; denied rows do not expose a cursor.
|
|
148
|
+
z.object({
|
|
149
|
+
operation: z.literal('lattice_runs_page'), mode, process: latticeId,
|
|
150
|
+
statuses: z.array(z.enum(ARTIFACT_PROCESS_RUN_STATUSES)).min(1).max(ARTIFACT_PROCESS_RUN_STATUSES.length)
|
|
151
|
+
.refine(value => new Set(value).size === value.length, 'Statuses must be unique').optional(),
|
|
152
|
+
entity: runPageEntity.optional(),
|
|
153
|
+
limit: z.number().int().min(1).max(ARTIFACT_PROCESS_RUNS_LIMIT).optional(),
|
|
154
|
+
cursor: z.string().min(1).max(2048).optional(),
|
|
155
|
+
archived: z.boolean().optional(),
|
|
156
|
+
}).strict(),
|
|
131
157
|
// Reads one run the user may view, as `GET /api/lattice/runs/[runId]` does: the run, its steps, the
|
|
132
158
|
// collapsed per-node states and the event cursor to resume its stream from.
|
|
133
159
|
z.object({ operation: z.literal('lattice_run'), mode, runId: latticeId }).strict(),
|
|
@@ -162,6 +188,22 @@ export const artifactServerRequestSchema = z.discriminatedUnion('operation', [
|
|
|
162
188
|
// The user's run `owner` relation decides; the node's assignee allowlist still applies.
|
|
163
189
|
z.object({ operation: z.literal('lattice_assign'), mode, runId: latticeId, stepId: latticeId, ...principalSet }).strict()
|
|
164
190
|
.refine(namesPrincipals, PRINCIPAL_SET_MESSAGE),
|
|
191
|
+
// Adds collaborators to selected, explicitly assigned human nodes and grants run viewing.
|
|
192
|
+
// Existing assignees remain. Onward sharing is explicit and scoped to the admitted application.
|
|
193
|
+
z.object({
|
|
194
|
+
operation: z.literal('lattice_share_run'), mode, runId: latticeId,
|
|
195
|
+
nodeIds: z.array(z.string().min(1).max(255).refine(value => !value.includes('::'), 'Node ids cannot contain the reserved :: separator'))
|
|
196
|
+
.min(1).max(ARTIFACT_PROCESS_SHARE_NODES_LIMIT),
|
|
197
|
+
...principalSet,
|
|
198
|
+
delegateSharing: z.object({ audience: z.literal('current_app') }).strict().optional(),
|
|
199
|
+
}).strict().refine(namesPrincipals, PRINCIPAL_SET_MESSAGE)
|
|
200
|
+
.refine(value => !value.delegateSharing || ((value.users?.length ?? 0) > 0 && (value.groups?.length ?? 0) === 0), 'Delegated sharing names individual users only'),
|
|
201
|
+
z.object({
|
|
202
|
+
operation: z.literal('lattice_sharing_authorities'), mode, runId: latticeId,
|
|
203
|
+
limit: z.number().int().min(1).max(ARTIFACT_PROCESS_SHARING_AUTHORITIES_LIMIT).optional(),
|
|
204
|
+
cursor: z.uuid().optional(),
|
|
205
|
+
}).strict(),
|
|
206
|
+
z.object({ operation: z.literal('lattice_sharing_authority_revoke'), mode, runId: latticeId, authorityId: z.uuid() }).strict(),
|
|
165
207
|
// Cancels a run of a process declared under `processes.act`, with its forked lineage, as
|
|
166
208
|
// `POST /api/lattice/runs/[runId]/cancel` does. The user's Invoker relation on the run decides.
|
|
167
209
|
z.object({ operation: z.literal('lattice_cancel'), mode, runId: latticeId }).strict(),
|
package/dist/server/client.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { StorageApiError, storageClient, storageErrorSchema } from '@sequenceholdings/storage';
|
|
3
3
|
import { OrmOperationError } from '@sequenceholdings/orm/client';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { artifactServerRequestSchema } from './broker-contract.js';
|
|
5
|
+
import { artifactOrmIdempotencyKeySchema, artifactServerRequestSchema } from './broker-contract.js';
|
|
6
6
|
import { createArtifactProcessesClient } from './processes.js';
|
|
7
7
|
import { assertJsonScalarsAcknowledged, ORM_JSON_SCALARS_HEADER } from '../orm-json-scalars.js';
|
|
8
8
|
import { createArtifactLogger } from './logger.js';
|
|
@@ -24,6 +24,7 @@ const maxBodyBytes = 1024 * 1024;
|
|
|
24
24
|
/** A read whose connection failed is sent at most three times, pausing up to 100 ms, then up to 200 ms. */
|
|
25
25
|
const reconnect = { attempts: 3, baseDelayMs: 100 };
|
|
26
26
|
export async function createArtifactServerClient({ headers, runtime, fetch: fetcher = globalThis.fetch, method, path, logSink, accept }) {
|
|
27
|
+
// An explicit runtime is re-parsed, so it never carries the development-lease marker.
|
|
27
28
|
const config = runtime ? artifactRuntimeConfigSchema.parse(runtime) : await readArtifactRuntimeConfig();
|
|
28
29
|
if (!config)
|
|
29
30
|
throw new ArtifactServerError(401, 'runtime_not_connected', 'Run seq-studio artifact dev to connect platform services, or use a hosted preview.');
|
|
@@ -32,7 +33,11 @@ export async function createArtifactServerClient({ headers, runtime, fetch: fetc
|
|
|
32
33
|
if (accept && isExternalContextToken(token)) {
|
|
33
34
|
if (!method || !path)
|
|
34
35
|
throw new ArtifactServerError(401, 'invalid_request_context', 'An external request context is verified only against the route method and path');
|
|
35
|
-
context = verifyArtifactExternalContext({
|
|
36
|
+
context = verifyArtifactExternalContext({
|
|
37
|
+
token, publicKeys: config.publicKeys, projectId: config.projectId, environment: config.environment, method, path, accept,
|
|
38
|
+
// A development context only under a development lease (never a hosted or explicit runtime).
|
|
39
|
+
developmentSessionId: config.development === true ? config.devSessionId : undefined,
|
|
40
|
+
});
|
|
36
41
|
}
|
|
37
42
|
else {
|
|
38
43
|
context = verifyArtifactRequestContext({ token, publicKeys: config.publicKeys, projectId: config.projectId, environment: config.environment, method, path });
|
|
@@ -42,7 +47,7 @@ export async function createArtifactServerClient({ headers, runtime, fetch: fetc
|
|
|
42
47
|
throw new ArtifactServerError(401, 'invalid_runtime_session', 'Artifact request does not belong to this runtime');
|
|
43
48
|
const getRuntime = async () => {
|
|
44
49
|
const current = runtime ? config : await readArtifactRuntimeConfig();
|
|
45
|
-
if (!current || current.projectId !== config.projectId || current.environment !== config.environment || current.atlasUrl !== config.atlasUrl || current.devSessionId !== config.devSessionId || current.deploymentId !== config.deploymentId)
|
|
50
|
+
if (!current || current.projectId !== config.projectId || current.environment !== config.environment || current.atlasUrl !== config.atlasUrl || current.devSessionId !== config.devSessionId || current.deploymentId !== config.deploymentId || current.development !== config.development)
|
|
46
51
|
throw new ArtifactServerError(401, 'runtime_changed', 'Artifact runtime disconnected or changed; reload the preview');
|
|
47
52
|
return current;
|
|
48
53
|
};
|
|
@@ -97,14 +102,9 @@ function buildClient(args) {
|
|
|
97
102
|
},
|
|
98
103
|
audit: { record: (event) => recordAuditEvent({ request, mode, event }) },
|
|
99
104
|
orm: ({ namespace, manifest, jsonScalars }) => ({
|
|
100
|
-
async execute(op, variables = {}) {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
if (!hash)
|
|
104
|
-
throw new ArtifactServerError(400, 'unknown_orm_operation', 'ORM operation is not in the generated manifest; run seq-studio orm generate');
|
|
105
|
-
const response = await request(artifactServerRequestSchema.parse({ operation: 'orm', mode, namespace, op: hash, variables, ...(jsonScalars ? { jsonScalars } : {}) }),
|
|
106
|
-
// Only an operation the manifest lists as a query is known to change nothing.
|
|
107
|
-
{ replaySafe: listed?.kind === 'query' });
|
|
105
|
+
async execute(op, variables = {}, options = {}) {
|
|
106
|
+
const { operation, replaySafe } = ormOperationRequest({ mode, namespace, manifest, op, variables, jsonScalars, idempotencyKey: options.idempotencyKey });
|
|
107
|
+
const response = await request(operation, { replaySafe });
|
|
108
108
|
const body = await response.json().catch(() => null);
|
|
109
109
|
const stepUp = stepUpError(response.status, body);
|
|
110
110
|
if (stepUp)
|
|
@@ -130,6 +130,30 @@ function buildClient(args) {
|
|
|
130
130
|
},
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* The broker `orm` request for one persisted operation, and whether a lost connection may resend
|
|
135
|
+
* it: an operation the manifest lists as a query changes nothing, and a mutation sent with an
|
|
136
|
+
* idempotency key is answered from its stored receipt when it arrives twice (the same body carries
|
|
137
|
+
* the same key and fingerprint).
|
|
138
|
+
*/
|
|
139
|
+
function ormOperationRequest({ mode, namespace, manifest, op, variables, jsonScalars, idempotencyKey }) {
|
|
140
|
+
const listed = op.startsWith('sha256:') ? Object.values(manifest.operations).find(entry => entry.hash === op) : manifest.operations[op];
|
|
141
|
+
const hash = op.startsWith('sha256:') ? op : listed?.hash;
|
|
142
|
+
if (!hash)
|
|
143
|
+
throw new ArtifactServerError(400, 'unknown_orm_operation', 'ORM operation is not in the generated manifest; run seq-studio orm generate');
|
|
144
|
+
const query = listed?.kind === 'query';
|
|
145
|
+
// A key names one mutation's receipt; the data plane refuses one on a query, so refuse it before the call.
|
|
146
|
+
if (idempotencyKey !== undefined && query)
|
|
147
|
+
throw new ArtifactServerError(400, 'idempotency_key_on_query', 'An idempotency key applies only to a mutation');
|
|
148
|
+
if (idempotencyKey !== undefined && !artifactOrmIdempotencyKeySchema.safeParse(idempotencyKey).success) {
|
|
149
|
+
throw new ArtifactServerError(400, 'invalid_idempotency_key', 'Idempotency keys are 1-128 letters, digits, dots, underscores, colons or hyphens');
|
|
150
|
+
}
|
|
151
|
+
const operation = artifactServerRequestSchema.parse({
|
|
152
|
+
operation: 'orm', mode, namespace, op: hash, variables,
|
|
153
|
+
...(jsonScalars ? { jsonScalars } : {}), ...(idempotencyKey === undefined ? {} : { idempotencyKey }),
|
|
154
|
+
});
|
|
155
|
+
return { operation, replaySafe: query || idempotencyKey !== undefined };
|
|
156
|
+
}
|
|
133
157
|
/**
|
|
134
158
|
* Sends one audit event, and once more with the same event id when Atlas couldn't confirm delivery
|
|
135
159
|
* (503, or no answer): Atlas derives the stored event id from it and the request, so the audit sink
|
|
@@ -231,8 +255,9 @@ function createBrokerTransport({ context, principal, token, getRuntime, fetcher,
|
|
|
231
255
|
/**
|
|
232
256
|
* Sends a read again when its connection to Atlas failed before Atlas answered: such failures
|
|
233
257
|
* clear within moments, and a read changes nothing by arriving twice. A write is never sent
|
|
234
|
-
* again, because a reset can come after Atlas acted on it
|
|
235
|
-
*
|
|
258
|
+
* again, because a reset can come after Atlas acted on it, unless its caller marks it replay-safe
|
|
259
|
+
* (an ORM mutation carrying an idempotency key). A response, whatever its status, is Atlas's
|
|
260
|
+
* answer and is returned as is.
|
|
236
261
|
*/
|
|
237
262
|
return async (input, { signal, replaySafe = isReadOperation(input), stream = false } = {}) => {
|
|
238
263
|
for (let attempt = 1;; attempt++) {
|
|
@@ -9,7 +9,7 @@ export interface ArtifactExternalAccept {
|
|
|
9
9
|
* bound to the one route it was signed for. Refuses a v1 context (`external_context_required`)
|
|
10
10
|
* and a context from an ingress the handler did not name (`external_ingress_not_accepted`).
|
|
11
11
|
*/
|
|
12
|
-
export declare function verifyArtifactExternalContext({ token, publicKeys, projectId, environment, method, path, accept, now }: {
|
|
12
|
+
export declare function verifyArtifactExternalContext({ token, publicKeys, projectId, environment, method, path, accept, developmentSessionId, now }: {
|
|
13
13
|
token: string | null;
|
|
14
14
|
publicKeys: ArtifactGatewayPublicKeys;
|
|
15
15
|
projectId: string;
|
|
@@ -17,5 +17,12 @@ export declare function verifyArtifactExternalContext({ token, publicKeys, proje
|
|
|
17
17
|
method: string;
|
|
18
18
|
path: string;
|
|
19
19
|
accept: ArtifactExternalAccept;
|
|
20
|
+
/**
|
|
21
|
+
* Set only when this runtime runs under a development lease (`runtimeConfig.development`): its
|
|
22
|
+
* session id. A `development`-mode context is accepted only then, and only for that session; a
|
|
23
|
+
* hosted runtime leaves it unset and refuses every development context
|
|
24
|
+
* (`external_mode_not_accepted`).
|
|
25
|
+
*/
|
|
26
|
+
developmentSessionId?: string;
|
|
20
27
|
now?: number;
|
|
21
28
|
}): RequestContextV2External;
|
|
@@ -6,11 +6,16 @@ import { verifyAnyArtifactContext } from './internal/verify-any.js';
|
|
|
6
6
|
* bound to the one route it was signed for. Refuses a v1 context (`external_context_required`)
|
|
7
7
|
* and a context from an ingress the handler did not name (`external_ingress_not_accepted`).
|
|
8
8
|
*/
|
|
9
|
-
export function verifyArtifactExternalContext({ token, publicKeys, projectId, environment, method, path, accept, now }) {
|
|
9
|
+
export function verifyArtifactExternalContext({ token, publicKeys, projectId, environment, method, path, accept, developmentSessionId, now }) {
|
|
10
10
|
if (!method || !path)
|
|
11
11
|
throw new ArtifactContextError();
|
|
12
12
|
// verifyAnyArtifactContext canonicalises `path` (canonicalizeIngressPath) before comparing it.
|
|
13
|
-
|
|
13
|
+
// A direct property, not a conditional spread: a spread member escapes excess-property
|
|
14
|
+
// checks, so a renamed verifier option would silently refuse every development context.
|
|
15
|
+
const context = verifyAnyArtifactContext({
|
|
16
|
+
token, publicKeys, projectId, environment, method, path, now,
|
|
17
|
+
development: developmentSessionId ? { sessionId: developmentSessionId } : undefined,
|
|
18
|
+
});
|
|
14
19
|
if (context.v !== 2)
|
|
15
20
|
throw new ArtifactContextError('external_context_required');
|
|
16
21
|
if (!accept.external.includes(context.principal.ingress))
|
|
@@ -15,7 +15,7 @@ export function parseV1Context({ payload, projectId, environment, method, path,
|
|
|
15
15
|
const context = parsed.data;
|
|
16
16
|
if (context.projectId !== projectId || context.environment !== environment ||
|
|
17
17
|
context.exp <= now || context.iat > now + 5 ||
|
|
18
|
-
(method !== undefined && context.method !== method.toUpperCase()) || (path !== undefined && context.path
|
|
18
|
+
(method !== undefined && context.method !== method.toUpperCase()) || (path !== undefined && !sameRequestTarget({ signed: context.path, actual: path })))
|
|
19
19
|
throw new ArtifactContextError();
|
|
20
20
|
Object.freeze(context.bridge);
|
|
21
21
|
Object.freeze(context.lattice);
|
|
@@ -24,3 +24,19 @@ export function parseV1Context({ payload, projectId, environment, method, path,
|
|
|
24
24
|
Object.freeze(context.taskRoutes);
|
|
25
25
|
return Object.freeze(context);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The gateway signs the raw request target; a hosted Next.js handler only sees its query
|
|
29
|
+
* re-serialized (`:` → `%3A`, a space → `+`). The pathname must match exactly; the query
|
|
30
|
+
* matches when it carries the same parameters, in the same order, however it is encoded.
|
|
31
|
+
*/
|
|
32
|
+
function sameRequestTarget({ signed, actual }) {
|
|
33
|
+
if (signed === actual)
|
|
34
|
+
return true;
|
|
35
|
+
const [signedPath, signedQuery = ''] = splitTarget(signed);
|
|
36
|
+
const [actualPath, actualQuery = ''] = splitTarget(actual);
|
|
37
|
+
return signedPath === actualPath && new URLSearchParams(signedQuery).toString() === new URLSearchParams(actualQuery).toString();
|
|
38
|
+
}
|
|
39
|
+
function splitTarget(target) {
|
|
40
|
+
const separator = target.indexOf('?');
|
|
41
|
+
return separator === -1 ? [target] : [target.slice(0, separator), target.slice(separator + 1)];
|
|
42
|
+
}
|
|
@@ -11,14 +11,23 @@ export type AnyArtifactRequestContext = ArtifactRequestContext | RequestContextV
|
|
|
11
11
|
* context must be signed by an `external-` kid. This pairing is the only thing standing between
|
|
12
12
|
* an `external-` kid and the v1 rules: the v1 branch goes straight to the shared v1 parser, not
|
|
13
13
|
* through the public verifier's own external gate. A v2 context in `development` mode is refused
|
|
14
|
-
*
|
|
14
|
+
* unless the caller names the one development session it serves (`development`), and then only
|
|
15
|
+
* for that session; the kid pairing above already confines it to an `external-` kid. The
|
|
16
|
+
* signature is checked once per call.
|
|
15
17
|
*/
|
|
16
|
-
export declare function verifyAnyArtifactContext({ token, publicKeys, projectId, environment, method, path, now }: {
|
|
18
|
+
export declare function verifyAnyArtifactContext({ token, publicKeys, projectId, environment, method, path, development, now }: {
|
|
17
19
|
token: string | null;
|
|
18
20
|
publicKeys: ArtifactGatewayPublicKeys;
|
|
19
21
|
projectId: string;
|
|
20
22
|
environment: string;
|
|
21
23
|
method?: string;
|
|
22
24
|
path?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The development session this caller serves. Pass it only from a development lease (the SDK)
|
|
27
|
+
* or, in Atlas, only under APP_ENV=development; never from a value the request carries.
|
|
28
|
+
*/
|
|
29
|
+
development?: {
|
|
30
|
+
sessionId: string;
|
|
31
|
+
};
|
|
23
32
|
now?: number;
|
|
24
33
|
}): AnyArtifactRequestContext;
|
|
@@ -13,9 +13,11 @@ import { canonicalizeIngressPath, signedIngressPath } from '../ingress-routes.js
|
|
|
13
13
|
* context must be signed by an `external-` kid. This pairing is the only thing standing between
|
|
14
14
|
* an `external-` kid and the v1 rules: the v1 branch goes straight to the shared v1 parser, not
|
|
15
15
|
* through the public verifier's own external gate. A v2 context in `development` mode is refused
|
|
16
|
-
*
|
|
16
|
+
* unless the caller names the one development session it serves (`development`), and then only
|
|
17
|
+
* for that session; the kid pairing above already confines it to an `external-` kid. The
|
|
18
|
+
* signature is checked once per call.
|
|
17
19
|
*/
|
|
18
|
-
export function verifyAnyArtifactContext({ token, publicKeys, projectId, environment, method, path, now = Math.floor(Date.now() / 1000) }) {
|
|
20
|
+
export function verifyAnyArtifactContext({ token, publicKeys, projectId, environment, method, path, development, now = Math.floor(Date.now() / 1000) }) {
|
|
19
21
|
let kid;
|
|
20
22
|
let payload;
|
|
21
23
|
try {
|
|
@@ -34,7 +36,7 @@ export function verifyAnyArtifactContext({ token, publicKeys, projectId, environ
|
|
|
34
36
|
if (!parsed.success)
|
|
35
37
|
throw new ArtifactContextError();
|
|
36
38
|
const context = parsed.data;
|
|
37
|
-
if (context
|
|
39
|
+
if (!modeAccepted({ context, development }))
|
|
38
40
|
throw new ArtifactContextError('external_mode_not_accepted');
|
|
39
41
|
// The gateway signs only a canonical path (plus its raw query); the caller's path is
|
|
40
42
|
// canonicalised the same way before comparing, so an encoded or dot-segment variant never binds.
|
|
@@ -46,6 +48,12 @@ export function verifyAnyArtifactContext({ token, publicKeys, projectId, environ
|
|
|
46
48
|
throw new ArtifactContextError();
|
|
47
49
|
return freezeExternalContext(context);
|
|
48
50
|
}
|
|
51
|
+
/** `published` always; `development` only for the one development session the caller names. */
|
|
52
|
+
function modeAccepted({ context, development }) {
|
|
53
|
+
if (context.mode === 'published')
|
|
54
|
+
return true;
|
|
55
|
+
return development !== undefined && context.devSessionId === development.sessionId;
|
|
56
|
+
}
|
|
49
57
|
function signedPathOf(raw) {
|
|
50
58
|
const canonical = canonicalizeIngressPath(raw);
|
|
51
59
|
return canonical && signedIngressPath(canonical);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type ArtifactServerMode, type ArtifactServerRequest } from './broker-contract.js';
|
|
2
|
+
/** Users (Auth0 `sub`) and chat groups (group id) to name in an assignment or a share. */
|
|
3
|
+
export interface ArtifactProcessPrincipals {
|
|
4
|
+
users?: string[];
|
|
5
|
+
groups?: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface ArtifactProcessRunViewerChange extends ArtifactProcessPrincipals {
|
|
8
|
+
runId: string;
|
|
9
|
+
}
|
|
10
|
+
/** Add collaborators to selected human nodes without replacing their existing run assignments. */
|
|
11
|
+
export interface ArtifactProcessShareRun extends ArtifactProcessPrincipals {
|
|
12
|
+
runId: string;
|
|
13
|
+
/** Explicitly assigned human nodes of this run, at most ARTIFACT_PROCESS_SHARE_NODES_LIMIT. */
|
|
14
|
+
nodeIds: string[];
|
|
15
|
+
/** Explicitly permit these users to share full-run viewing and the same or fewer nodes within this app's live audience. */
|
|
16
|
+
delegateSharing?: {
|
|
17
|
+
audience: 'current_app';
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface ArtifactProcessShareRunResult {
|
|
21
|
+
run_id: string;
|
|
22
|
+
nodes_updated: string[];
|
|
23
|
+
users: string[];
|
|
24
|
+
groups: string[];
|
|
25
|
+
/** True when the sharing grants have been applied; pending authorization rejects with 503. */
|
|
26
|
+
authorization_applied: boolean;
|
|
27
|
+
/** The root authority created or used by an explicitly delegated share. */
|
|
28
|
+
sharingAuthorityId?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ArtifactProcessRunSharingAuthoritiesQuery {
|
|
31
|
+
runId: string;
|
|
32
|
+
/** Default 20, at most ARTIFACT_PROCESS_SHARING_AUTHORITIES_LIMIT (50). */
|
|
33
|
+
limit?: number;
|
|
34
|
+
/** Authority id from the previous page's nextCursor. */
|
|
35
|
+
cursor?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ArtifactProcessRunSharingDelegate {
|
|
38
|
+
user_sub: string;
|
|
39
|
+
node_ids: string[];
|
|
40
|
+
granted_by: string;
|
|
41
|
+
granted_at: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ArtifactProcessRunSharingAuthority {
|
|
44
|
+
id: string;
|
|
45
|
+
run_id: string;
|
|
46
|
+
bundle_hash: string;
|
|
47
|
+
node_ids: string[];
|
|
48
|
+
audience: {
|
|
49
|
+
kind: 'artifact_project';
|
|
50
|
+
project_id: string;
|
|
51
|
+
};
|
|
52
|
+
created_by: string;
|
|
53
|
+
created_at: string;
|
|
54
|
+
revoked_at: string | null;
|
|
55
|
+
/** At most 100 individual delegates under this root authority. */
|
|
56
|
+
delegates: ArtifactProcessRunSharingDelegate[];
|
|
57
|
+
}
|
|
58
|
+
export interface ArtifactProcessRunSharingAuthoritiesPage {
|
|
59
|
+
authorities: ArtifactProcessRunSharingAuthority[];
|
|
60
|
+
nextCursor: string | null;
|
|
61
|
+
}
|
|
62
|
+
export interface ArtifactProcessRevokeRunSharingAuthority {
|
|
63
|
+
runId: string;
|
|
64
|
+
authorityId: string;
|
|
65
|
+
}
|
|
66
|
+
export interface ArtifactProcessRevokeRunSharingAuthorityResult {
|
|
67
|
+
authority_id: string;
|
|
68
|
+
revoked: true;
|
|
69
|
+
}
|
|
70
|
+
export interface ArtifactRunSharingOperations {
|
|
71
|
+
/** Add collaborators and full-run viewing; owners may explicitly authorize scoped onward sharing. */
|
|
72
|
+
shareRun(input: ArtifactProcessShareRun): Promise<ArtifactProcessShareRunResult>;
|
|
73
|
+
/** Owner-only, user-mode page of this run's sharing authorities and their delegates. */
|
|
74
|
+
listRunSharingAuthorities(query: ArtifactProcessRunSharingAuthoritiesQuery): Promise<ArtifactProcessRunSharingAuthoritiesPage>;
|
|
75
|
+
/** Owner-only, idempotent revocation of onward sharing; existing viewing and step access remain. */
|
|
76
|
+
revokeRunSharingAuthority(input: ArtifactProcessRevokeRunSharingAuthority): Promise<ArtifactProcessRevokeRunSharingAuthorityResult>;
|
|
77
|
+
}
|
|
78
|
+
export declare function shareArtifactProcessRun({ mode, json, input }: {
|
|
79
|
+
mode: ArtifactServerMode;
|
|
80
|
+
json: (operation: ArtifactServerRequest) => Promise<unknown>;
|
|
81
|
+
input: ArtifactProcessShareRun;
|
|
82
|
+
}): Promise<ArtifactProcessShareRunResult>;
|
|
83
|
+
export declare function createArtifactProcessSharingClient({ mode, json }: {
|
|
84
|
+
mode: ArtifactServerMode;
|
|
85
|
+
json: (operation: ArtifactServerRequest) => Promise<unknown>;
|
|
86
|
+
}): ArtifactRunSharingOperations;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { artifactServerRequestSchema } from './broker-contract.js';
|
|
2
|
+
import { ArtifactServerError } from './errors.js';
|
|
3
|
+
export async function shareArtifactProcessRun({ mode, json, input }) {
|
|
4
|
+
// Atlas owns assignment validation; its wire result names the optional authority in snake case.
|
|
5
|
+
const body = await json(artifactServerRequestSchema.parse({ operation: 'lattice_share_run', mode, ...input }));
|
|
6
|
+
const { sharing_authority_id, ...shared } = body;
|
|
7
|
+
if (sharing_authority_id !== undefined && typeof sharing_authority_id !== 'string') {
|
|
8
|
+
throw new ArtifactServerError(502, 'malformed_response', 'Atlas returned a malformed sharing authority id');
|
|
9
|
+
}
|
|
10
|
+
return { ...shared, ...(sharing_authority_id === undefined ? {} : { sharingAuthorityId: sharing_authority_id }) };
|
|
11
|
+
}
|
|
12
|
+
export function createArtifactProcessSharingClient({ mode, json }) {
|
|
13
|
+
return {
|
|
14
|
+
shareRun: (input) => shareArtifactProcessRun({ mode, json, input }),
|
|
15
|
+
listRunSharingAuthorities: async (query) => {
|
|
16
|
+
// Atlas returns the owner-authorized records; the SDK maps only the continuation envelope.
|
|
17
|
+
const body = await json(artifactServerRequestSchema.parse({ operation: 'lattice_sharing_authorities', mode, ...query }));
|
|
18
|
+
if (!Array.isArray(body?.authorities) || !(typeof body.next_cursor === 'string' || body.next_cursor === null)) {
|
|
19
|
+
throw new ArtifactServerError(502, 'malformed_response', 'Atlas returned a malformed sharing authority page');
|
|
20
|
+
}
|
|
21
|
+
// Record projection and authority checks belong to Atlas, as with the other process lists.
|
|
22
|
+
return { authorities: body.authorities, nextCursor: body.next_cursor };
|
|
23
|
+
},
|
|
24
|
+
revokeRunSharingAuthority: async (input) => {
|
|
25
|
+
// The owner-authorized service returns its idempotent revocation receipt.
|
|
26
|
+
return await json(artifactServerRequestSchema.parse({ operation: 'lattice_sharing_authority_revoke', mode, ...input }));
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ArtifactProcessRunStatus, type ArtifactServerMode, type ArtifactServerRequest } from './broker-contract.js';
|
|
2
|
+
import type { ArtifactProcessRunRecord } from './processes.js';
|
|
3
|
+
/**
|
|
4
|
+
* Page visible runs of one declared process; filters stay the same while following its cursor.
|
|
5
|
+
* Atlas establishes the caller's complete authorization scope before selecting run rows.
|
|
6
|
+
* Unavailable authorization, an unsupported authorization model, or an incomplete grant scope
|
|
7
|
+
* returns 503 without partial runs or a cursor.
|
|
8
|
+
*/
|
|
9
|
+
export interface ArtifactProcessRunsPageQuery {
|
|
10
|
+
process: string;
|
|
11
|
+
statuses?: ArtifactProcessRunStatus[];
|
|
12
|
+
/** One entity type, optionally narrowed to one id or a set of up to 500 ids. */
|
|
13
|
+
entity?: {
|
|
14
|
+
type: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
ids?: string[];
|
|
17
|
+
};
|
|
18
|
+
/** Maximum returned visible runs per page, default 100 and at most ARTIFACT_PROCESS_RUNS_LIMIT (500). */
|
|
19
|
+
limit?: number;
|
|
20
|
+
/** Encrypted continuation bound to this principal and these filters; expires after one hour. */
|
|
21
|
+
cursor?: string;
|
|
22
|
+
/** Soft-archived runs only, instead of the default active ones. */
|
|
23
|
+
archived?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** A compact activity-list row; read the run separately for its state, output and steps. */
|
|
26
|
+
export type ArtifactProcessRunSummary = Pick<ArtifactProcessRunRecord, 'id' | 'processId' | 'status' | 'entityType' | 'entityId' | 'startedAt' | 'completedAt' | 'error'>;
|
|
27
|
+
export interface ArtifactProcessRunsPage {
|
|
28
|
+
runs: ArtifactProcessRunSummary[];
|
|
29
|
+
/**
|
|
30
|
+
* Continues after the last returned visible run only when another visible run exists.
|
|
31
|
+
* Null ends the list; a successful empty page always has null.
|
|
32
|
+
*/
|
|
33
|
+
nextCursor: string | null;
|
|
34
|
+
}
|
|
35
|
+
export declare function readArtifactProcessRunsPage({ mode, json, query }: {
|
|
36
|
+
mode: ArtifactServerMode;
|
|
37
|
+
json: (operation: ArtifactServerRequest) => Promise<unknown>;
|
|
38
|
+
query: ArtifactProcessRunsPageQuery;
|
|
39
|
+
}): Promise<ArtifactProcessRunsPage>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { artifactServerRequestSchema } from './broker-contract.js';
|
|
2
|
+
import { ArtifactServerError } from './errors.js';
|
|
3
|
+
export async function readArtifactProcessRunsPage({ mode, json, query }) {
|
|
4
|
+
const body = await json(artifactServerRequestSchema.parse({ operation: 'lattice_runs_page', mode, ...query }));
|
|
5
|
+
if (!Array.isArray(body?.runs) || !(typeof body.nextCursor === 'string' || body.nextCursor === null)) {
|
|
6
|
+
throw new ArtifactServerError(502, 'malformed_response', 'Atlas returned a malformed run page');
|
|
7
|
+
}
|
|
8
|
+
// Atlas owns row projection and visibility filtering; the SDK preserves its continuation.
|
|
9
|
+
return body;
|
|
10
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { type ArtifactProcessPrincipalType, type ArtifactProcessRunStatus, type ArtifactProcessTaskView, type ArtifactServerMode, type ArtifactServerRequest } from './broker-contract.js';
|
|
2
|
+
import { type ArtifactProcessRunsPage, type ArtifactProcessRunsPageQuery } from './process-runs-page.js';
|
|
3
|
+
import { type ArtifactProcessPrincipals, type ArtifactProcessRunViewerChange, type ArtifactRunSharingOperations } from './process-run-sharing.js';
|
|
4
|
+
export type { ArtifactProcessRunSummary, ArtifactProcessRunsPage, ArtifactProcessRunsPageQuery } from './process-runs-page.js';
|
|
5
|
+
export type { ArtifactProcessPrincipals, ArtifactProcessRunViewerChange, ArtifactProcessShareRun, ArtifactProcessShareRunResult } from './process-run-sharing.js';
|
|
6
|
+
export type { ArtifactProcessRunSharingAuthoritiesQuery, ArtifactProcessRunSharingAuthoritiesPage, ArtifactProcessRunSharingAuthority, ArtifactProcessRunSharingDelegate, ArtifactProcessRevokeRunSharingAuthority, ArtifactProcessRevokeRunSharingAuthorityResult } from './process-run-sharing.js';
|
|
2
7
|
/** A run of a process declared under `server.capabilities.processes.start`. */
|
|
3
8
|
export interface ArtifactProcessStart {
|
|
4
9
|
process: string;
|
|
@@ -264,11 +269,6 @@ export interface ArtifactProcessRunEvents {
|
|
|
264
269
|
/** Pass as `since` on the next poll. Null only when the run has no events yet and no `since` was given. */
|
|
265
270
|
cursor: string | null;
|
|
266
271
|
}
|
|
267
|
-
/** Users (Auth0 `sub`) and chat groups (group id) to name in an assignment or a share. */
|
|
268
|
-
export interface ArtifactProcessPrincipals {
|
|
269
|
-
users?: string[];
|
|
270
|
-
groups?: string[];
|
|
271
|
-
}
|
|
272
272
|
/** Reassigning a parked human step: the step's advancers become exactly these principals. */
|
|
273
273
|
export interface ArtifactProcessAssign extends ArtifactProcessPrincipals {
|
|
274
274
|
runId: string;
|
|
@@ -288,9 +288,6 @@ export interface ArtifactProcessRunViewers {
|
|
|
288
288
|
users: string[];
|
|
289
289
|
groups: string[];
|
|
290
290
|
}
|
|
291
|
-
export interface ArtifactProcessRunViewerChange extends ArtifactProcessPrincipals {
|
|
292
|
-
runId: string;
|
|
293
|
-
}
|
|
294
291
|
export interface ArtifactProcessPrincipalSearch {
|
|
295
292
|
/** Empty browses the first `limit` users; 1–2 characters match groups only; 3+ search the directory. */
|
|
296
293
|
q: string;
|
|
@@ -309,11 +306,13 @@ export interface ArtifactProcessPrincipal {
|
|
|
309
306
|
/** Groups: active members. */
|
|
310
307
|
memberCount?: number;
|
|
311
308
|
}
|
|
312
|
-
export interface ArtifactServerProcesses {
|
|
309
|
+
export interface ArtifactServerProcesses extends ArtifactRunSharingOperations {
|
|
313
310
|
/** Start a run of a process declared under `processes.start`; the user's Invoker grant decides. */
|
|
314
311
|
start(input: ArtifactProcessStart): Promise<ArtifactProcessRun>;
|
|
315
312
|
/** List the runs of a process declared under `processes.read` that the user may view. */
|
|
316
313
|
listRuns(query: ArtifactProcessRunsQuery): Promise<ArtifactProcessRunRecord[]>;
|
|
314
|
+
/** Page compact run summaries the user may view; nextCursor names a continuation only when more visible runs exist. */
|
|
315
|
+
listRunsPage(query: ArtifactProcessRunsPageQuery): Promise<ArtifactProcessRunsPage>;
|
|
317
316
|
/** Read one run of a process declared under `processes.read`; the user's Viewer relation on the run decides. */
|
|
318
317
|
getRun(runId: string): Promise<ArtifactProcessRunSnapshot>;
|
|
319
318
|
/** Describe a process declared under `processes.read`; the user's Viewer relation on the process decides. */
|
package/dist/server/processes.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { artifactServerRequestSchema, } from './broker-contract.js';
|
|
2
2
|
import { ArtifactServerError } from './errors.js';
|
|
3
|
+
import { readArtifactProcessRunsPage } from './process-runs-page.js';
|
|
4
|
+
import { createArtifactProcessSharingClient } from './process-run-sharing.js';
|
|
3
5
|
/** `client.processes`: Lattice through the broker's typed `lattice*` operations. Atlas owns validation and redaction. */
|
|
4
6
|
export function createArtifactProcessesClient({ mode, json }) {
|
|
5
7
|
return {
|
|
@@ -14,6 +16,7 @@ export function createArtifactProcessesClient({ mode, json }) {
|
|
|
14
16
|
throw new ArtifactServerError(502, 'malformed_response', 'Atlas returned a malformed run list');
|
|
15
17
|
return body.runs;
|
|
16
18
|
},
|
|
19
|
+
listRunsPage: (query) => readArtifactProcessRunsPage({ mode, json, query }),
|
|
17
20
|
getRun: async (runId) => {
|
|
18
21
|
// The Lattice run-detail response; Atlas owns redaction and node-state derivation.
|
|
19
22
|
return await json(artifactServerRequestSchema.parse({ operation: 'lattice_run', mode, runId }));
|
|
@@ -36,6 +39,7 @@ export function createArtifactProcessesClient({ mode, json }) {
|
|
|
36
39
|
assign: async (input) => {
|
|
37
40
|
return await json(artifactServerRequestSchema.parse({ operation: 'lattice_assign', mode, ...input }));
|
|
38
41
|
},
|
|
42
|
+
...createArtifactProcessSharingClient({ mode, json }),
|
|
39
43
|
cancel: async (runId) => {
|
|
40
44
|
return await json(artifactServerRequestSchema.parse({ operation: 'lattice_cancel', mode, runId }));
|
|
41
45
|
},
|
|
@@ -72,8 +72,15 @@ export const ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX = 'service-account|';
|
|
|
72
72
|
export const ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS = 60;
|
|
73
73
|
/** An IPv4 or IPv6 address, as the gateway observed it. The control contract reuses it. */
|
|
74
74
|
export const artifactIpAddressSchema = z.union([z.ipv4(), z.ipv6()]);
|
|
75
|
+
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
|
|
75
76
|
/** Signed by the gateway after it strips control characters and truncates; refused here otherwise. */
|
|
76
|
-
const observedUserAgent = z.string().max(512).refine(value =>
|
|
77
|
+
const observedUserAgent = z.string().max(512).refine(value => !CONTROL_CHARACTERS.test(value), 'Expected a user agent without control characters');
|
|
78
|
+
/**
|
|
79
|
+
* The principal's subject and email become the ORM's row-policy claims (`ext_subject`, `ext_email`),
|
|
80
|
+
* which refuse control characters; refusing them here keeps such a context a 401 at verification
|
|
81
|
+
* rather than a failure in the middle of a request.
|
|
82
|
+
*/
|
|
83
|
+
const externalSubject = id.refine(value => !CONTROL_CHARACTERS.test(value), 'Expected a subject without control characters');
|
|
77
84
|
/** The external caller as the gateway verified it at pass exchange. Strict: see the v2 key policy below. */
|
|
78
85
|
export const artifactExternalPrincipalSchema = z.object({
|
|
79
86
|
kind: z.literal('external'),
|
|
@@ -81,10 +88,10 @@ export const artifactExternalPrincipalSchema = z.object({
|
|
|
81
88
|
ingress: id,
|
|
82
89
|
provider: id,
|
|
83
90
|
issuer: z.string().min(1).max(2048),
|
|
84
|
-
subject:
|
|
91
|
+
subject: externalSubject,
|
|
85
92
|
clientId: id,
|
|
86
93
|
/** 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'),
|
|
94
|
+
email: z.string().min(3).max(320).refine(value => value === value.trim().toLowerCase() && value.includes('@') && !CONTROL_CHARACTERS.test(value), 'Expected a normalized email'),
|
|
88
95
|
emailVerified: z.literal(true),
|
|
89
96
|
authTime: z.number().int().nonnegative().nullable(),
|
|
90
97
|
amr: z.array(z.string().min(1).max(64)).max(16).nullable(),
|
|
@@ -8,7 +8,16 @@ export declare const artifactRuntimeConfigSchema: z.ZodObject<{
|
|
|
8
8
|
devSessionId: z.ZodOptional<z.ZodString>;
|
|
9
9
|
deploymentId: z.ZodOptional<z.ZodString>;
|
|
10
10
|
}, z.core.$strict>;
|
|
11
|
-
export type ArtifactRuntimeConfig = z.infer<typeof artifactRuntimeConfigSchema
|
|
11
|
+
export type ArtifactRuntimeConfig = z.infer<typeof artifactRuntimeConfigSchema> & {
|
|
12
|
+
/**
|
|
13
|
+
* True only on a configuration read from a development lease: the owner-only file `artifact dev`
|
|
14
|
+
* writes and names in SEQUENCE_ARTIFACT_RUNTIME_FILE (`next-project.ts`), for one development
|
|
15
|
+
* session. Never parsed from JSON, so neither the lease file, the hosted configuration nor an
|
|
16
|
+
* explicit `runtime` option can claim it, and it does not survive a re-parse. Only under it does
|
|
17
|
+
* the SDK accept an external context in `development` mode.
|
|
18
|
+
*/
|
|
19
|
+
readonly development?: true;
|
|
20
|
+
};
|
|
12
21
|
export type ArtifactRequestHeaders = Pick<Headers, 'get'>;
|
|
13
22
|
/** Credentials are platform-provided and rotated by the CLI, never inferred from ambient cloud auth. */
|
|
14
23
|
export declare function readArtifactRuntimeConfig({ file }?: {
|