@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,245 @@
1
+ import { randomBytes, randomUUID } from 'node:crypto';
2
+ import { performance } from 'node:perf_hooks';
3
+ import { z } from 'zod';
4
+ import { isManagedServerInvokePath } from '../server/managed-server-path.js';
5
+ import { ARTIFACT_CONTEXT_MAX_AGE_SECONDS, ARTIFACT_LATTICE_CONTEXT_MAX_AGE_SECONDS } from '../server/request-context.js';
6
+ import { normalizePublicOrigin } from '../tunnel/protocol.js';
7
+ import { ArtifactAdmissionError } from './admission.js';
8
+ import { GatewayControlError } from './control.js';
9
+ import { invokeHostedArtifact } from './lattice-upstream.js';
10
+ import { signGatewayContext } from './session.js';
11
+ const requestSchema = z.object({
12
+ server: z.string().min(1).max(255),
13
+ path: z.string().refine(isManagedServerInvokePath, 'Invalid managed server path'),
14
+ principalSub: z.string().min(1).max(255),
15
+ body: z.unknown().optional(),
16
+ lattice: z.object({
17
+ runId: z.string().min(1).max(255),
18
+ nodeId: z.string().min(1).max(255),
19
+ stepId: z.string().min(1).max(255),
20
+ processId: z.string().min(1).max(255),
21
+ }).strict(),
22
+ requestId: z.string().min(1).max(255),
23
+ timeoutMs: z.number().int().positive().max(300_000),
24
+ }).strict();
25
+ const BODY_CAP = Math.floor(1.1 * 1024 * 1024);
26
+ /** Lattice invoke, then the generic refusal for every other `/_sequence/` path. */
27
+ export async function routeSequenceRequest(input) {
28
+ if (await routeLatticeInvoke(input))
29
+ return true;
30
+ if (input.request.url?.startsWith('/_sequence/')) {
31
+ write(input.response, 404, { error: 'not_found' });
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+ export async function routeLatticeInvoke(input) {
37
+ if ((input.request.url ?? '').split('?')[0] !== '/_sequence/lattice-invoke')
38
+ return false;
39
+ if (input.request.method !== 'POST') {
40
+ write(input.response, 404, { error: 'not_found' });
41
+ return true;
42
+ }
43
+ await handleLatticeInvoke(input);
44
+ return true;
45
+ }
46
+ export async function handleLatticeInvoke(input) {
47
+ const startedAt = new Date().toISOString();
48
+ const startedMs = performance.now();
49
+ const presented = bearer(input.request);
50
+ if (!presented || !input.control.tokenMatches(presented)) {
51
+ write(input.response, 403, { error: 'lattice_invoke_denied' });
52
+ return;
53
+ }
54
+ let parsed;
55
+ try {
56
+ parsed = requestSchema.parse(JSON.parse((await readBody(input.request)).toString('utf8')));
57
+ }
58
+ catch {
59
+ write(input.response, 400, { error: 'invalid_lattice_invoke' });
60
+ return;
61
+ }
62
+ let grant;
63
+ try {
64
+ grant = await input.control.latticeInvoke({
65
+ server: parsed.server,
66
+ path: parsed.path,
67
+ principalSub: parsed.principalSub,
68
+ lattice: parsed.lattice,
69
+ requestId: parsed.requestId,
70
+ });
71
+ }
72
+ catch (error) {
73
+ const status = error instanceof GatewayControlError ? error.status : 502;
74
+ if (status === 503) {
75
+ write(input.response, 503, { error: 'lattice_invoke_unavailable' });
76
+ return;
77
+ }
78
+ if (status === 401 || status === 403 || status === 404) {
79
+ write(input.response, 403, { error: 'lattice_invoke_denied' });
80
+ return;
81
+ }
82
+ write(input.response, 502, { error: 'lattice_invoke_failed' });
83
+ return;
84
+ }
85
+ if (grant.principalSub !== parsed.principalSub || grant.path !== parsed.path) {
86
+ write(input.response, 403, { error: 'lattice_invoke_denied' });
87
+ return;
88
+ }
89
+ const observed = await invokeAdmittedLattice({ ...input, grant, parsed });
90
+ input.usage?.record(latticeUsageEvent({ grant, requestId: parsed.requestId, startedAt, startedMs, ...observed }));
91
+ }
92
+ /** The browser proxy records on response finish. This path returns first, so it records the upstream result itself. */
93
+ async function invokeAdmittedLattice(input) {
94
+ const signal = callerAbortSignal(input);
95
+ let release = () => { };
96
+ try {
97
+ const admitted = await acquireLatticeAdmission({ admission: input.admission, grant: input.grant });
98
+ release = admitted.bind(input.response, () => input.response.destroy());
99
+ const session = syntheticSession(input.grant, input.parsed.lattice);
100
+ const requestContext = signGatewayContext({
101
+ session,
102
+ method: 'POST',
103
+ path: input.grant.path,
104
+ privateKey: input.privateKey,
105
+ keyId: input.keyId,
106
+ serviceAccountId: input.grant.serviceAccountId,
107
+ requestId: input.parsed.requestId,
108
+ ttlSeconds: Math.min(ARTIFACT_LATTICE_CONTEXT_MAX_AGE_SECONDS, Math.max(ARTIFACT_CONTEXT_MAX_AGE_SECONDS, Math.ceil(input.parsed.timeoutMs / 1000))),
109
+ });
110
+ const result = await (input.invoke ?? invokeHostedArtifact)({
111
+ upstreamUrl: input.grant.upstreamUrl,
112
+ ...(input.grant.upstreamAudience ? { upstreamAudience: input.grant.upstreamAudience } : {}),
113
+ path: input.grant.path,
114
+ body: input.parsed.body ?? {},
115
+ requestContext,
116
+ timeoutMs: input.parsed.timeoutMs,
117
+ signal,
118
+ });
119
+ write(input.response, 200, { status: result.status, body: result.body });
120
+ return latticeOutcome({ aborted: signal.aborted, status: result.status });
121
+ }
122
+ catch (error) {
123
+ if (error instanceof ArtifactAdmissionError) {
124
+ writeAdmissionFailure(input.response, error);
125
+ return { outcome: 'error', httpStatus: error.status };
126
+ }
127
+ if (signal.aborted || input.response.writableEnded)
128
+ return { outcome: 'cancelled', httpStatus: 499 };
129
+ write(input.response, 502, { error: 'lattice_invoke_failed' });
130
+ return { outcome: 'error', httpStatus: 502 };
131
+ }
132
+ finally {
133
+ release();
134
+ }
135
+ }
136
+ function latticeOutcome({ aborted, status }) {
137
+ const httpStatus = status >= 100 && status <= 599 ? status : 502;
138
+ if (aborted)
139
+ return { outcome: 'cancelled', httpStatus };
140
+ return { outcome: httpStatus >= 400 ? 'error' : 'success', httpStatus };
141
+ }
142
+ function latticeUsageEvent(input) {
143
+ return {
144
+ // One id per gateway call. Retries of the same Lattice step reuse requestId,
145
+ // and a later attempt with a different outcome would be rejected as a
146
+ // conflicting duplicate. Delivery retries resend this same object.
147
+ eventId: `gateway:${randomUUID()}`,
148
+ requestId: input.requestId,
149
+ actorId: input.grant.principalSub,
150
+ projectId: input.grant.projectId,
151
+ deploymentId: input.grant.deploymentId,
152
+ environment: input.grant.environment,
153
+ startedAt: input.startedAt,
154
+ endedAt: new Date().toISOString(),
155
+ measurements: { requestCount: 1, durationMs: Math.max(0, Math.round(performance.now() - input.startedMs)) },
156
+ outcome: input.outcome,
157
+ httpStatus: input.httpStatus,
158
+ };
159
+ }
160
+ function writeAdmissionFailure(response, error) {
161
+ const capacity = error.status === 429;
162
+ write(response, capacity ? 429 : 503, {
163
+ error: capacity ? 'lattice_invoke_capacity' : 'lattice_invoke_unavailable',
164
+ });
165
+ }
166
+ /** Same per-app lease the browser proxy acquires. Missing admission fails closed. */
167
+ async function acquireLatticeAdmission(input) {
168
+ if (!input.admission)
169
+ throw new ArtifactAdmissionError(503, 'admission_unavailable');
170
+ return input.admission.acquire({
171
+ environment: input.grant.environment,
172
+ projectId: input.grant.projectId,
173
+ deploymentId: input.grant.deploymentId,
174
+ resources: input.grant.resources,
175
+ });
176
+ }
177
+ /** The worker closing its connection must abort the Cloud Run POST still in flight. */
178
+ function callerAbortSignal(input) {
179
+ const abort = new AbortController();
180
+ const stop = () => {
181
+ if (!input.response.writableEnded)
182
+ abort.abort();
183
+ };
184
+ if (typeof input.request.on === 'function')
185
+ input.request.on('aborted', stop);
186
+ if (typeof input.response.on === 'function')
187
+ input.response.on('close', stop);
188
+ return abort.signal;
189
+ }
190
+ function syntheticSession(grant, lattice) {
191
+ const publicOrigin = normalizePublicOrigin(grant.publicOrigin);
192
+ return {
193
+ version: 1,
194
+ kind: 'hosted',
195
+ sessionId: null,
196
+ deploymentId: grant.deploymentId,
197
+ frameworkBuildId: grant.frameworkBuildId,
198
+ projectId: grant.projectId,
199
+ ownerId: grant.principalSub,
200
+ environment: grant.environment,
201
+ publicOrigin,
202
+ expiresAt: Date.now() + 60_000,
203
+ published: true,
204
+ bridge: {
205
+ parentOrigin: publicOrigin,
206
+ channelId: 'lattice-invoke',
207
+ nonce: randomBytes(16).toString('hex'),
208
+ },
209
+ lattice,
210
+ };
211
+ }
212
+ function bearer(request) {
213
+ const header = request.headers.authorization;
214
+ if (typeof header !== 'string' || !header.startsWith('Bearer '))
215
+ return null;
216
+ return header.slice('Bearer '.length);
217
+ }
218
+ function readBody(request) {
219
+ return new Promise((resolve, reject) => {
220
+ const chunks = [];
221
+ let length = 0;
222
+ request.on('data', (chunk) => {
223
+ length += chunk.byteLength;
224
+ if (length > BODY_CAP) {
225
+ reject(new Error('body too large'));
226
+ request.destroy();
227
+ return;
228
+ }
229
+ chunks.push(chunk);
230
+ });
231
+ request.on('end', () => resolve(Buffer.concat(chunks)));
232
+ request.on('error', reject);
233
+ });
234
+ }
235
+ function write(response, status, body) {
236
+ if (response.headersSent)
237
+ return;
238
+ response.writeHead(status, {
239
+ ...(status >= 400 ? { connection: 'close' } : {}),
240
+ 'content-type': 'application/json',
241
+ 'cache-control': 'no-store',
242
+ 'x-content-type-options': 'nosniff',
243
+ });
244
+ response.end(JSON.stringify(body));
245
+ }
@@ -0,0 +1,38 @@
1
+ import { request as httpsRequest } from 'node:https';
2
+ export interface HostedArtifactInvocation {
3
+ upstreamUrl: string;
4
+ upstreamAudience?: string;
5
+ path: string;
6
+ body: unknown;
7
+ requestContext: string;
8
+ timeoutMs: number;
9
+ /** Wall-clock cancellation from the caller, combined with `timeoutMs`. */
10
+ signal?: AbortSignal;
11
+ identityTokenProvider?: (input: {
12
+ audience: string;
13
+ signal: AbortSignal;
14
+ }) => Promise<string>;
15
+ /** Tests supply the socket. Production uses `https.request`. */
16
+ dial?: typeof httpsRequest;
17
+ /** Tests supply the hop. Production dials the validated Cloud Run origin. */
18
+ dispatch?: (input: {
19
+ origin: URL;
20
+ path: string;
21
+ headers: Record<string, string>;
22
+ body: Buffer;
23
+ timeoutMs: number;
24
+ signal: AbortSignal;
25
+ }) => Promise<{
26
+ status: number;
27
+ contentType?: string;
28
+ body: Buffer;
29
+ }>;
30
+ }
31
+ /**
32
+ * Buffered POST into a hosted Next.js app. The destination must already be a
33
+ * Cloud Run origin Atlas resolved; this rejects anything else before dialing.
34
+ */
35
+ export declare function invokeHostedArtifact(input: HostedArtifactInvocation): Promise<{
36
+ status: number;
37
+ body: unknown;
38
+ }>;
@@ -0,0 +1,136 @@
1
+ import { request as httpsRequest } from 'node:https';
2
+ import { assertSafeRequest } from '../tunnel/protocol.js';
3
+ import { isManagedServerInvokePath } from '../server/managed-server-path.js';
4
+ import { HOSTED_ARTIFACT_PROXY_LIMITS, cloudRunOrigin, hostedIdentityToken } from './hosted-proxy.js';
5
+ const BODY_CAP = 1024 * 1024;
6
+ /**
7
+ * Buffered POST into a hosted Next.js app. The destination must already be a
8
+ * Cloud Run origin Atlas resolved; this rejects anything else before dialing.
9
+ */
10
+ export async function invokeHostedArtifact(input) {
11
+ if (!isManagedServerInvokePath(input.path))
12
+ throw new Error('Invalid managed server path');
13
+ const upstream = cloudRunOrigin(input.upstreamUrl);
14
+ const audience = input.upstreamAudience === undefined ? upstream : cloudRunOrigin(input.upstreamAudience);
15
+ assertSafeRequest('POST', input.path);
16
+ const payload = Buffer.from(JSON.stringify(input.body ?? {}));
17
+ if (payload.byteLength > BODY_CAP)
18
+ throw new Error('Managed server body exceeds 1 MiB');
19
+ const timeoutMs = Math.min(Math.max(input.timeoutMs, 1), HOSTED_ARTIFACT_PROXY_LIMITS.maxTimeoutMs);
20
+ const signal = invocationSignal({ timeoutMs, signal: input.signal });
21
+ const token = await withSignal((input.identityTokenProvider ?? hostedIdentityToken)({ audience: audience.origin, signal }), signal);
22
+ if (!/^[A-Za-z0-9._~-]+$/.test(token) || token.length > 16384)
23
+ throw new Error('Invalid platform token');
24
+ const headers = {
25
+ host: upstream.host,
26
+ 'content-type': 'application/json',
27
+ 'content-length': String(payload.byteLength),
28
+ 'accept-encoding': 'identity',
29
+ 'x-serverless-authorization': `Bearer ${token}`,
30
+ 'x-sequence-artifact-context': input.requestContext,
31
+ };
32
+ const hop = { origin: upstream, path: input.path, headers, body: payload, timeoutMs, signal };
33
+ const response = await withSignal(input.dispatch ? input.dispatch(hop) : postCloudRun({ ...hop, dial: input.dial }), signal);
34
+ if (response.body.byteLength > BODY_CAP)
35
+ throw new Error('Managed server response exceeds 1 MiB');
36
+ return { status: response.status, body: decodeBody(response) };
37
+ }
38
+ function decodeBody(response) {
39
+ if (response.body.byteLength === 0)
40
+ return null;
41
+ const text = response.body.toString('utf8');
42
+ if (response.contentType?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json')
43
+ return text;
44
+ try {
45
+ return JSON.parse(text);
46
+ }
47
+ catch {
48
+ return text;
49
+ }
50
+ }
51
+ /**
52
+ * `timeoutMs` is a deadline from the start of the call, not socket idle time.
53
+ * Streaming a chunk forever must not extend it. `signal` is the worker's
54
+ * disconnect; either one destroys the Cloud Run request.
55
+ */
56
+ function invocationSignal({ timeoutMs, signal }) {
57
+ const deadline = AbortSignal.timeout(timeoutMs);
58
+ return signal ? AbortSignal.any([signal, deadline]) : deadline;
59
+ }
60
+ function invocationAbortError(signal) {
61
+ const reason = signal.reason;
62
+ if (reason instanceof Error && reason.name === 'TimeoutError')
63
+ return new Error('Managed server invocation timed out');
64
+ if (reason instanceof Error && /timed out/i.test(reason.message))
65
+ return reason;
66
+ return new Error('Managed server invocation cancelled');
67
+ }
68
+ function withSignal(work, signal) {
69
+ if (signal.aborted)
70
+ return Promise.reject(invocationAbortError(signal));
71
+ return new Promise((resolve, reject) => {
72
+ const onAbort = () => reject(invocationAbortError(signal));
73
+ signal.addEventListener('abort', onAbort, { once: true });
74
+ work.then((value) => { signal.removeEventListener('abort', onAbort); resolve(value); }, (error) => { signal.removeEventListener('abort', onAbort); reject(error); });
75
+ });
76
+ }
77
+ function postCloudRun(input) {
78
+ return new Promise((resolve, reject) => {
79
+ if (input.signal.aborted) {
80
+ reject(invocationAbortError(input.signal));
81
+ return;
82
+ }
83
+ let settled = false;
84
+ let incoming;
85
+ let onAbort = () => { };
86
+ const stop = () => input.signal.removeEventListener('abort', onAbort);
87
+ const fail = (error) => {
88
+ if (settled)
89
+ return;
90
+ settled = true;
91
+ stop();
92
+ incoming?.destroy();
93
+ request.destroy();
94
+ reject(error instanceof Error ? error : new Error('Managed server invocation failed'));
95
+ };
96
+ const succeed = (value) => {
97
+ if (settled)
98
+ return;
99
+ settled = true;
100
+ stop();
101
+ resolve(value);
102
+ };
103
+ const request = (input.dial ?? httpsRequest)(input.origin, {
104
+ method: 'POST',
105
+ path: input.path,
106
+ headers: input.headers,
107
+ timeout: input.timeoutMs,
108
+ }, (response) => {
109
+ incoming = response;
110
+ const chunks = [];
111
+ let length = 0;
112
+ response.on('data', (chunk) => {
113
+ length += chunk.byteLength;
114
+ if (length > BODY_CAP) {
115
+ fail(new Error('Managed server response exceeds 1 MiB'));
116
+ return;
117
+ }
118
+ chunks.push(chunk);
119
+ });
120
+ response.on('end', () => {
121
+ const contentType = response.headers['content-type'];
122
+ succeed({
123
+ status: response.statusCode ?? 502,
124
+ ...(typeof contentType === 'string' ? { contentType } : {}),
125
+ body: Buffer.concat(chunks),
126
+ });
127
+ });
128
+ response.on('error', fail);
129
+ });
130
+ onAbort = () => fail(invocationAbortError(input.signal));
131
+ input.signal.addEventListener('abort', onAbort, { once: true });
132
+ request.on('error', fail);
133
+ request.on('timeout', () => fail(new Error('Managed server invocation timed out')));
134
+ request.end(input.body);
135
+ });
136
+ }
@@ -6,6 +6,7 @@ import { assertSafeRequest, normalizePublicOrigin } from '../tunnel/protocol.js'
6
6
  import { GatewayControlError } from './control.js';
7
7
  import { decodeGatewaySession, gatewaySessionCookies, renewGatewaySession, signGatewayContext } from './session.js';
8
8
  import { proxyHostedArtifact } from './hosted-proxy.js';
9
+ import { routeSequenceRequest } from './lattice-invoke.js';
9
10
  import { admitHostedMutation, ReleaseAdmissionError } from './release-admission.js';
10
11
  import { ArtifactAdmissionError } from './admission.js';
11
12
  import { gatewayClientAddress } from './control-admission.js';
@@ -285,10 +286,8 @@ export function createArtifactGateway(options) {
285
286
  await exchangeTicket({ request, response, origin });
286
287
  return;
287
288
  }
288
- if (request.url?.startsWith('/_sequence/')) {
289
- json(response, 404, 'not_found');
289
+ if (await routeSequenceRequest({ request, response, control: options.control, privateKey: options.contextPrivateKey, keyId: options.contextKeyId, admission: options.admission, usage }))
290
290
  return;
291
- }
292
291
  if (['GET', 'HEAD'].includes(request.method ?? 'GET')) {
293
292
  const url = new URL(request.url ?? '/', origin);
294
293
  if (url.searchParams.has('__sequence_release')) {
@@ -71,7 +71,7 @@ export declare function decodeGatewaySession(input: {
71
71
  frameworkBuildId?: string;
72
72
  }): GatewaySession | null;
73
73
  /** A fresh jti on every signature: a context names one originating request and its method. */
74
- export declare function signGatewayContext({ session, method, path, privateKey, keyId, now, capabilities, serviceAccountId, requestId }: {
74
+ export declare function signGatewayContext({ session, method, path, privateKey, keyId, now, capabilities, serviceAccountId, requestId, ttlSeconds }: {
75
75
  session: GatewaySession;
76
76
  method: string;
77
77
  path: string;
@@ -81,5 +81,7 @@ export declare function signGatewayContext({ session, method, path, privateKey,
81
81
  capabilities?: string[];
82
82
  serviceAccountId?: string | null;
83
83
  requestId?: string;
84
+ /** Seconds until exp. Browser requests stay at 60; a Lattice run may last up to 300. */
85
+ ttlSeconds?: number;
84
86
  }): string;
85
87
  export {};
@@ -1,6 +1,6 @@
1
1
  import { createHmac, createPrivateKey, randomUUID, sign, timingSafeEqual } from 'node:crypto';
2
2
  import { z } from 'zod';
3
- import { artifactRequestContextSchema } from '../server/request-context.js';
3
+ import { ARTIFACT_CONTEXT_MAX_AGE_SECONDS, ARTIFACT_LATTICE_CONTEXT_MAX_AGE_SECONDS, artifactRequestContextSchema } from '../server/request-context.js';
4
4
  import { normalizePublicOrigin } from '../tunnel/protocol.js';
5
5
  import { artifactNextFrameworkBuildIdSchema } from '../runtime-contracts.js';
6
6
  export const GATEWAY_SESSION_COOKIE = '__Host-atlas-artifact-session';
@@ -137,13 +137,20 @@ export function decodeGatewaySession(input) {
137
137
  return matches.sort((left, right) => right.expiresAt - left.expiresAt || (left.deploymentId ?? '').localeCompare(right.deploymentId ?? ''))[0] ?? null;
138
138
  }
139
139
  /** A fresh jti on every signature: a context names one originating request and its method. */
140
- export function signGatewayContext({ session, method, path, privateKey, keyId, now = Math.floor(Date.now() / 1000), capabilities = [], serviceAccountId = null, requestId = randomUUID() }) {
140
+ export function signGatewayContext({ session, method, path, privateKey, keyId, now = Math.floor(Date.now() / 1000), capabilities = [], serviceAccountId = null, requestId = randomUUID(), ttlSeconds = ARTIFACT_CONTEXT_MAX_AGE_SECONDS }) {
141
+ const latticeRun = session.lattice?.runId ?? null;
142
+ const ceiling = latticeRun !== null && latticeRun !== ''
143
+ ? ARTIFACT_LATTICE_CONTEXT_MAX_AGE_SECONDS
144
+ : ARTIFACT_CONTEXT_MAX_AGE_SECONDS;
145
+ if (!Number.isInteger(ttlSeconds) || ttlSeconds < 1 || ttlSeconds > ceiling) {
146
+ throw new Error(`Artifact context lifetime must be 1..${ceiling} seconds`);
147
+ }
141
148
  const context = artifactRequestContextSchema.parse({
142
149
  v: 1, iss: 'atlas-artifact-gateway', aud: `artifact:${session.projectId}`,
143
150
  sub: session.ownerId, projectId: session.projectId, environment: session.environment,
144
151
  mode: session.kind === 'development' ? 'development' : session.published ? 'published' : 'preview',
145
152
  deploymentId: session.deploymentId, devSessionId: session.sessionId, requestId, jti: randomUUID(),
146
- method, path, iat: now, exp: now + 60, capabilities, serviceAccountId,
153
+ method, path, iat: now, exp: now + ttlSeconds, capabilities, serviceAccountId,
147
154
  bridge: session.bridge, theme: 'system',
148
155
  lattice: session.lattice ?? { runId: null, nodeId: null, stepId: null, processId: null },
149
156
  ...(session.taskRoutes ? { taskRoutes: session.taskRoutes } : {}),
@@ -20,7 +20,18 @@ export declare const ARTIFACT_NEXT_RESOURCE_DEFAULTS: {
20
20
  max_instances: number;
21
21
  timeout_seconds: number;
22
22
  };
23
+ /**
24
+ * Where the server's audit events (`client.audit.record`) go: an Atlas audit service id. Every
25
+ * application may record under `artifact_studio`; Atlas admits a first-party service, such as
26
+ * `application_portal`, only for the projects it registers for it. Server-only: browsers can't audit.
27
+ */
28
+ export declare const artifactNextAuditCapabilitySchema: z.ZodObject<{
29
+ service: z.ZodString;
30
+ }, z.core.$strict>;
23
31
  export declare const artifactNextServerCapabilitiesSchema: z.ZodPrefault<z.ZodObject<{
32
+ audit: z.ZodOptional<z.ZodObject<{
33
+ service: z.ZodString;
34
+ }, z.core.$strict>>;
24
35
  storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
25
36
  viewer: "viewer";
26
37
  editor: "editor";
@@ -37,6 +48,11 @@ export declare const artifactNextServerCapabilitiesSchema: z.ZodPrefault<z.ZodOb
37
48
  functions: z.ZodPrefault<z.ZodObject<{
38
49
  invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
39
50
  }, z.core.$strict>>;
51
+ processes: z.ZodPrefault<z.ZodObject<{
52
+ start: z.ZodDefault<z.ZodArray<z.ZodString>>;
53
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
54
+ act: z.ZodDefault<z.ZodArray<z.ZodString>>;
55
+ }, z.core.$strict>>;
40
56
  }, z.core.$strict>>;
41
57
  export type ArtifactNextServerCapabilities = z.infer<typeof artifactNextServerCapabilitiesSchema>;
42
58
  export declare const artifactNextResourceLimitsSchema: z.ZodPrefault<z.ZodObject<{
@@ -70,6 +86,9 @@ export declare const artifactNextBuildConfigSchema: z.ZodPrefault<z.ZodObject<{
70
86
  }, z.core.$strict>>;
71
87
  export declare const artifactNextServerSchema: z.ZodPrefault<z.ZodObject<{
72
88
  capabilities: z.ZodPrefault<z.ZodObject<{
89
+ audit: z.ZodOptional<z.ZodObject<{
90
+ service: z.ZodString;
91
+ }, z.core.$strict>>;
73
92
  storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
74
93
  viewer: "viewer";
75
94
  editor: "editor";
@@ -86,6 +105,11 @@ export declare const artifactNextServerSchema: z.ZodPrefault<z.ZodObject<{
86
105
  functions: z.ZodPrefault<z.ZodObject<{
87
106
  invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
88
107
  }, z.core.$strict>>;
108
+ processes: z.ZodPrefault<z.ZodObject<{
109
+ start: z.ZodDefault<z.ZodArray<z.ZodString>>;
110
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
111
+ act: z.ZodDefault<z.ZodArray<z.ZodString>>;
112
+ }, z.core.$strict>>;
89
113
  }, z.core.$strict>>;
90
114
  service_account: z.ZodOptional<z.ZodString>;
91
115
  secrets: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -105,8 +129,18 @@ export declare const artifactNextServerSchema: z.ZodPrefault<z.ZodObject<{
105
129
  cache: z.ZodOptional<z.ZodObject<{
106
130
  capacity_gb: z.ZodNumber;
107
131
  }, z.core.$strict>>;
132
+ lattice: z.ZodOptional<z.ZodObject<{
133
+ accept: z.ZodDefault<z.ZodArray<z.ZodString>>;
134
+ }, z.core.$strict>>;
108
135
  }, z.core.$strict>>;
109
136
  export declare const artifactStudioNextManifestSchema: z.ZodObject<{
137
+ ontology: z.ZodOptional<z.ZodObject<{
138
+ version: z.ZodLiteral<1>;
139
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
140
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
141
+ produce: z.ZodDefault<z.ZodArray<z.ZodString>>;
142
+ invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
143
+ }, z.core.$strict>>;
110
144
  bundle: z.ZodObject<{
111
145
  name: z.ZodString;
112
146
  schema_version: z.ZodLiteral<2>;
@@ -155,6 +189,11 @@ export declare const artifactStudioNextManifestSchema: z.ZodObject<{
155
189
  functions: z.ZodPrefault<z.ZodObject<{
156
190
  invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
157
191
  }, z.core.$strict>>;
192
+ processes: z.ZodPrefault<z.ZodObject<{
193
+ start: z.ZodDefault<z.ZodArray<z.ZodString>>;
194
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
195
+ act: z.ZodDefault<z.ZodArray<z.ZodString>>;
196
+ }, z.core.$strict>>;
158
197
  }, z.core.$strict>>;
159
198
  build: z.ZodPrefault<z.ZodObject<{
160
199
  public_env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -166,6 +205,9 @@ export declare const artifactStudioNextManifestSchema: z.ZodObject<{
166
205
  }, z.core.$strict>>;
167
206
  server: z.ZodPrefault<z.ZodObject<{
168
207
  capabilities: z.ZodPrefault<z.ZodObject<{
208
+ audit: z.ZodOptional<z.ZodObject<{
209
+ service: z.ZodString;
210
+ }, z.core.$strict>>;
169
211
  storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
170
212
  viewer: "viewer";
171
213
  editor: "editor";
@@ -182,6 +224,11 @@ export declare const artifactStudioNextManifestSchema: z.ZodObject<{
182
224
  functions: z.ZodPrefault<z.ZodObject<{
183
225
  invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
184
226
  }, z.core.$strict>>;
227
+ processes: z.ZodPrefault<z.ZodObject<{
228
+ start: z.ZodDefault<z.ZodArray<z.ZodString>>;
229
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
230
+ act: z.ZodDefault<z.ZodArray<z.ZodString>>;
231
+ }, z.core.$strict>>;
185
232
  }, z.core.$strict>>;
186
233
  service_account: z.ZodOptional<z.ZodString>;
187
234
  secrets: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -201,10 +248,18 @@ export declare const artifactStudioNextManifestSchema: z.ZodObject<{
201
248
  cache: z.ZodOptional<z.ZodObject<{
202
249
  capacity_gb: z.ZodNumber;
203
250
  }, z.core.$strict>>;
251
+ lattice: z.ZodOptional<z.ZodObject<{
252
+ accept: z.ZodDefault<z.ZodArray<z.ZodString>>;
253
+ }, z.core.$strict>>;
204
254
  }, z.core.$strict>>;
205
255
  targets: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
206
256
  url: z.ZodString;
207
257
  }, z.core.$strict>>>;
258
+ presentation: z.ZodOptional<z.ZodObject<{
259
+ mobile: z.ZodOptional<z.ZodEnum<{
260
+ responsive: "responsive";
261
+ }>>;
262
+ }, z.core.$strip>>;
208
263
  sidebarIcon: z.ZodOptional<z.ZodString>;
209
264
  sidebarBadge: z.ZodOptional<z.ZodObject<{
210
265
  endpoint: z.ZodString;