@sequenceholdings/artifact-studio 0.8.0 → 0.9.0
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 +101 -3
- package/dist/api.d.ts +2 -1
- package/dist/api.js +15 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +58 -20
- package/dist/gateway/authorization-cache.js +1 -1
- package/dist/gateway/control.d.ts +13 -1
- package/dist/gateway/control.js +3 -0
- package/dist/gateway/datadog-startup.d.ts +13 -0
- package/dist/gateway/datadog-startup.js +51 -0
- package/dist/gateway/hosted-proxy.d.ts +2 -0
- package/dist/gateway/hosted-proxy.js +9 -11
- package/dist/gateway/ingress-handler.js +2 -0
- package/dist/gateway/server.d.ts +1 -1
- package/dist/gateway/server.js +51 -20
- package/dist/gateway/session.d.ts +14 -0
- package/dist/gateway/session.js +31 -2
- package/dist/gateway-main.js +5 -3
- package/dist/next-build.d.ts +3 -1
- package/dist/next-build.js +10 -6
- package/dist/sdk.d.ts +2 -0
- package/dist/server/agent-responses.d.ts +425 -0
- package/dist/server/agent-responses.js +154 -0
- package/dist/server/broker-contract.d.ts +5 -0
- package/dist/server/broker-contract.js +6 -2
- package/dist/server/client.d.ts +2 -0
- package/dist/server/client.js +10 -2
- package/dist/server/processes.d.ts +11 -0
- package/dist/server/request-authentication.d.ts +42 -0
- package/dist/server/request-authentication.js +93 -0
- package/dist/server/request-context.d.ts +1 -0
- package/dist/server/request-context.js +1 -0
- package/dist/server.d.ts +2 -1
- package/dist/server.js +1 -1
- package/dist/templates/react-vite/package.json +1 -1
- package/dist/tunnel/broker.d.ts +14 -1
- package/dist/tunnel/broker.js +94 -10
- package/dist/tunnel/client.js +6 -0
- package/dist/tunnel/redis-types.d.ts +5 -1
- package/dist/tunnel/redis.d.ts +12 -0
- package/dist/tunnel/redis.js +102 -4
- package/dist/tunnel/transport.d.ts +3 -0
- package/package.json +3 -3
- package/templates/react-vite/package.json +1 -1
package/dist/gateway/server.js
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
import { ArtifactTunnelBroker } from '../tunnel/index.js';
|
|
5
5
|
import { assertSafeRequest, normalizePublicOrigin } from '../tunnel/protocol.js';
|
|
6
6
|
import { GatewayControlError } from './control.js';
|
|
7
|
-
import { decodeGatewaySession, gatewaySessionCookies, renewGatewaySession,
|
|
7
|
+
import { decodeGatewaySession, gatewaySessionCookies, renewGatewaySession, signGatewayRequest } from './session.js';
|
|
8
8
|
import { proxyHostedArtifact } from './hosted-proxy.js';
|
|
9
9
|
import { bootstrapPage } from './bootstrap-page.js';
|
|
10
10
|
import { routeSequenceRequest } from './lattice-invoke.js';
|
|
@@ -95,7 +95,9 @@ export function createArtifactGateway(options) {
|
|
|
95
95
|
const authorizations = new PositiveDecisionCache({ ttlMs: options.authorizationCacheTtlMs });
|
|
96
96
|
const tunnelAuthorizations = new PositiveDecisionCache({ ttlMs: options.authorizationCacheTtlMs });
|
|
97
97
|
const usage = new GatewayUsageReporter({ send: events => options.control.usage(events), onFailure: failure => options.onError?.(failure) });
|
|
98
|
+
const requests = new Set();
|
|
98
99
|
let draining = false;
|
|
100
|
+
let drainPromise;
|
|
99
101
|
const isIngressRequest = (request) => resemblesIngressHost({ rawHost: header(request, 'host'), runtimeDomain: options.runtimeDomain });
|
|
100
102
|
const staticFiles = options.staticFiles ?? new StaticArtifactFiles({ fetch: ({ deploymentId, path }) => options.control.staticFile({ deploymentId, path, parentOrigin }) });
|
|
101
103
|
/** Admission for the unauthenticated endpoints; the release frees the in-flight permit. */
|
|
@@ -173,10 +175,11 @@ export function createArtifactGateway(options) {
|
|
|
173
175
|
const target = await decision.promise;
|
|
174
176
|
const requestId = randomUUID();
|
|
175
177
|
// A static deployment has no server of its own to read a request context.
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
const signed = target.kind === 'static' ? { requestContext: '' } : signGatewayRequest({
|
|
179
|
+
session: { ...session, lattice: target.lattice, taskRoutes: target.taskRoutes }, method, path: request.url ?? '/', privateKey: options.contextPrivateKey, keyId: options.contextKeyId,
|
|
180
|
+
capabilities: target.capabilities, serviceAccountId: target.serviceAccountId, requestId,
|
|
181
|
+
});
|
|
182
|
+
const result = { session, target, requestId, authorizationCache: decision.source, ...signed };
|
|
180
183
|
activeRequests.set(request, result);
|
|
181
184
|
return result;
|
|
182
185
|
}
|
|
@@ -206,7 +209,8 @@ export function createArtifactGateway(options) {
|
|
|
206
209
|
const authorized = await authorize(request);
|
|
207
210
|
if (authorized.target.kind !== 'development' || authorized.target.sessionId !== sessionId)
|
|
208
211
|
return null;
|
|
209
|
-
return { ownerId: authorized.session.ownerId, publicOrigin: authorized.session.publicOrigin, requestContext: authorized.requestContext
|
|
212
|
+
return { ownerId: authorized.session.ownerId, publicOrigin: authorized.session.publicOrigin, requestContext: authorized.requestContext,
|
|
213
|
+
...(authorized.requestAuthentication ? { requestAuthentication: authorized.requestAuthentication } : {}) };
|
|
210
214
|
},
|
|
211
215
|
validateLease: async (lease) => {
|
|
212
216
|
const credential = leases.get(lease.sessionId);
|
|
@@ -247,14 +251,24 @@ export function createArtifactGateway(options) {
|
|
|
247
251
|
const proxyDevelopment = options.ingress?.appEnv === 'development' ? createDevelopmentIngressProxy({ broker }) : undefined;
|
|
248
252
|
const ingress = createIngressHandler({ options: options.ingress, runtimeDomain: options.runtimeDomain, protocol: new URL(gatewayOrigin).protocol, isDraining: () => draining, onError: options.onError, usage, proxyDevelopment });
|
|
249
253
|
const server = createServer({ maxHeaderSize: 32 * 1024, requestTimeout: 60_000, headersTimeout: 15_000 }, (request, response) => {
|
|
254
|
+
// server.close() can run before a destroyed response emits close. Join that event's
|
|
255
|
+
// usage finalizer AND the handler: authorization may finish after the browser disconnects.
|
|
256
|
+
const finished = new Promise(resolve => {
|
|
257
|
+
response.once('finish', resolve);
|
|
258
|
+
response.once('close', resolve);
|
|
259
|
+
});
|
|
260
|
+
const track = (handler) => {
|
|
261
|
+
const settled = Promise.allSettled([handler, finished]).then(() => { requests.delete(settled); });
|
|
262
|
+
requests.add(settled);
|
|
263
|
+
};
|
|
250
264
|
// Before readiness, bootstrap and requestOrigin(): an ingress host, canonical or not, never
|
|
251
265
|
// reaches a session-path handler or the 401/404→403 remap below (design §(e) "Branch placement").
|
|
252
266
|
if (isIngressRequest(request)) {
|
|
253
|
-
|
|
267
|
+
track(ingress.handle(request, response));
|
|
254
268
|
return;
|
|
255
269
|
}
|
|
256
270
|
const startedAt = new Date().toISOString(), started = performance.now();
|
|
257
|
-
|
|
271
|
+
track((async () => {
|
|
258
272
|
if (isReadinessRequest({ request, gatewayOrigin, runtimeDomain: options.runtimeDomain })) {
|
|
259
273
|
const ready = !draining && (options.ready?.() ?? true);
|
|
260
274
|
json(response, ready ? 200 : 503, ready ? 'ready' : 'unavailable');
|
|
@@ -294,7 +308,7 @@ export function createArtifactGateway(options) {
|
|
|
294
308
|
}
|
|
295
309
|
}
|
|
296
310
|
const authorizationStarted = performance.now();
|
|
297
|
-
const { session, target, requestContext, requestId, authorizationCache } = await authorize(request);
|
|
311
|
+
const { session, target, requestContext, requestAuthentication, requestId, authorizationCache } = await authorize(request);
|
|
298
312
|
// Immutable files Atlas already authorized this session for: no admission, context or usage.
|
|
299
313
|
if (target.kind === 'static') {
|
|
300
314
|
await serveStaticTarget({ request, response, target, files: staticFiles, renew: () => renewCookie({ options, request, response, session, target }), parentOrigin: session.bridge.parentOrigin });
|
|
@@ -340,7 +354,7 @@ export function createArtifactGateway(options) {
|
|
|
340
354
|
if (response.destroyed)
|
|
341
355
|
return;
|
|
342
356
|
const { body } = await admitHostedMutation({ request, expectedDeploymentId: target.deploymentId, expectedFrameworkBuildId: target.frameworkBuildId, active: target.active, published: session.published });
|
|
343
|
-
await proxyHostedArtifact({ request, response, target, requestContext, body, limits: { timeoutMs: admitted.timeoutMs }, gatewayTimings });
|
|
357
|
+
await proxyHostedArtifact({ request, response, target, requestContext, requestAuthentication, body, limits: { timeoutMs: admitted.timeoutMs }, gatewayTimings });
|
|
344
358
|
})().catch(error => {
|
|
345
359
|
options.onError?.({ kind: error instanceof ReleaseAdmissionError ? 'release_mismatch' : error instanceof GatewayControlError ? 'authorization_denied'
|
|
346
360
|
: error instanceof ArtifactAdmissionError ? 'admission_rejected' : 'request_failed' });
|
|
@@ -348,7 +362,7 @@ export function createArtifactGateway(options) {
|
|
|
348
362
|
if (error instanceof ArtifactAdmissionError && error.retryAfterSeconds && !response.headersSent)
|
|
349
363
|
response.setHeader('retry-after', String(error.retryAfterSeconds));
|
|
350
364
|
json(response, status === 401 || status === 404 ? 403 : status, error instanceof ReleaseAdmissionError ? 'update_required' : 'artifact_request_failed');
|
|
351
|
-
});
|
|
365
|
+
}));
|
|
352
366
|
});
|
|
353
367
|
server.on('upgrade', (request, socket, head) => {
|
|
354
368
|
void (async () => {
|
|
@@ -366,6 +380,10 @@ export function createArtifactGateway(options) {
|
|
|
366
380
|
return;
|
|
367
381
|
}
|
|
368
382
|
const authorized = await authorize(request);
|
|
383
|
+
if (draining) {
|
|
384
|
+
rejectUpgrade(socket, 503);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
369
387
|
if (authorized.target.kind !== 'development') {
|
|
370
388
|
rejectUpgrade(socket);
|
|
371
389
|
return;
|
|
@@ -378,18 +396,31 @@ export function createArtifactGateway(options) {
|
|
|
378
396
|
handleClientErrors({ server, ingress });
|
|
379
397
|
return {
|
|
380
398
|
server,
|
|
381
|
-
/**
|
|
399
|
+
/** Recorded observations not yet confirmed by Atlas; excludes responses still in flight. */
|
|
382
400
|
get pendingUsage() { return usage.pending; },
|
|
383
|
-
|
|
401
|
+
drain() {
|
|
402
|
+
if (drainPromise)
|
|
403
|
+
return drainPromise;
|
|
384
404
|
draining = true;
|
|
405
|
+
broker.beginDrain();
|
|
406
|
+
const closed = new Promise(resolve => server.close(() => resolve()));
|
|
385
407
|
server.closeIdleConnections();
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
408
|
+
drainPromise = (async () => {
|
|
409
|
+
while (requests.size)
|
|
410
|
+
await Promise.all(requests);
|
|
411
|
+
// Active responses may have become idle since close() first stopped admission.
|
|
412
|
+
server.closeIdleConnections();
|
|
413
|
+
// Keep lease checks and revocation live until local and remote HTTP streams finish.
|
|
414
|
+
await broker.drain();
|
|
415
|
+
await closed;
|
|
416
|
+
while (requests.size)
|
|
417
|
+
await Promise.all(requests);
|
|
418
|
+
leases.clear();
|
|
419
|
+
authorizations.clear();
|
|
420
|
+
tunnelAuthorizations.clear();
|
|
421
|
+
await usage.close();
|
|
422
|
+
})();
|
|
423
|
+
return drainPromise;
|
|
393
424
|
},
|
|
394
425
|
};
|
|
395
426
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const GATEWAY_SESSION_COOKIE = "__Host-atlas-artifact-session";
|
|
3
3
|
export declare function gatewayReleaseCookieName(deploymentId: string): string;
|
|
4
|
+
/** Only the authenticated Atlas bootstrap control reply supplies this private cookie member. */
|
|
5
|
+
export declare const gatewayAuthenticationSchema: z.ZodObject<{
|
|
6
|
+
method: z.ZodLiteral<"auth0_user">;
|
|
7
|
+
sub: z.ZodString;
|
|
8
|
+
}, z.core.$strict>;
|
|
4
9
|
declare const sessionSchema: z.ZodObject<{
|
|
5
10
|
version: z.ZodLiteral<1>;
|
|
6
11
|
kind: z.ZodEnum<{
|
|
@@ -30,6 +35,10 @@ declare const sessionSchema: z.ZodObject<{
|
|
|
30
35
|
processId: z.ZodNullable<z.ZodString>;
|
|
31
36
|
}, z.core.$strict>>;
|
|
32
37
|
taskRoutes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
38
|
+
authentication: z.ZodOptional<z.ZodObject<{
|
|
39
|
+
method: z.ZodLiteral<"auth0_user">;
|
|
40
|
+
sub: z.ZodString;
|
|
41
|
+
}, z.core.$strict>>;
|
|
33
42
|
}, z.core.$strict>;
|
|
34
43
|
export type GatewaySession = z.infer<typeof sessionSchema>;
|
|
35
44
|
/** Active use renews the idle window, but never extends the original admission's eight-hour bound. */
|
|
@@ -85,4 +94,9 @@ export declare function signGatewayContext({ session, method, path, privateKey,
|
|
|
85
94
|
/** Seconds until exp. Browser requests stay at 60; a Lattice run may last up to 300. */
|
|
86
95
|
ttlSeconds?: number;
|
|
87
96
|
}): string;
|
|
97
|
+
/** Signs a proof only for the exact context this call just minted from its authenticated session. */
|
|
98
|
+
export declare function signGatewayRequest(input: Parameters<typeof signGatewayContext>[0]): {
|
|
99
|
+
requestContext: string;
|
|
100
|
+
requestAuthentication?: string;
|
|
101
|
+
};
|
|
88
102
|
export {};
|
package/dist/gateway/session.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { createHmac, createPrivateKey, randomUUID, sign, timingSafeEqual } from 'node:crypto';
|
|
1
|
+
import { createHash, createHmac, createPrivateKey, randomUUID, sign, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { z } from 'zod';
|
|
3
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
|
+
import { ARTIFACT_REQUEST_AUTHENTICATION_MAX_LENGTH } from '../server/request-authentication.js';
|
|
6
7
|
export const GATEWAY_SESSION_COOKIE = '__Host-atlas-artifact-session';
|
|
7
8
|
export function gatewayReleaseCookieName(deploymentId) {
|
|
8
9
|
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(deploymentId))
|
|
@@ -16,6 +17,8 @@ catch {
|
|
|
16
17
|
return false;
|
|
17
18
|
} });
|
|
18
19
|
const id = z.string().min(1).max(255);
|
|
20
|
+
/** Only the authenticated Atlas bootstrap control reply supplies this private cookie member. */
|
|
21
|
+
export const gatewayAuthenticationSchema = z.object({ method: z.literal('auth0_user'), sub: id }).strict();
|
|
19
22
|
const sessionSchema = z.object({
|
|
20
23
|
version: z.literal(1), kind: z.enum(['development', 'hosted', 'static']),
|
|
21
24
|
sessionId: id.nullable(), deploymentId: id.nullable(),
|
|
@@ -24,6 +27,7 @@ const sessionSchema = z.object({
|
|
|
24
27
|
bridge: artifactRequestContextSchema.shape.bridge,
|
|
25
28
|
lattice: artifactRequestContextSchema.shape.lattice.optional(),
|
|
26
29
|
taskRoutes: artifactRequestContextSchema.shape.taskRoutes,
|
|
30
|
+
authentication: gatewayAuthenticationSchema.optional(),
|
|
27
31
|
}).strict().refine(value => value.kind === 'development'
|
|
28
32
|
? value.sessionId !== null && value.deploymentId === null && value.frameworkBuildId === null && !value.published
|
|
29
33
|
// A static deployment has no Next build: its assets are named by the deployment itself.
|
|
@@ -33,7 +37,8 @@ const sessionSchema = z.object({
|
|
|
33
37
|
function sameSessionScope(left, right) {
|
|
34
38
|
return left.ownerId === right.ownerId && left.projectId === right.projectId &&
|
|
35
39
|
left.environment === right.environment && left.publicOrigin === right.publicOrigin &&
|
|
36
|
-
left.kind === right.kind && left.published === right.published
|
|
40
|
+
left.kind === right.kind && left.published === right.published &&
|
|
41
|
+
left.authentication?.method === right.authentication?.method && left.authentication?.sub === right.authentication?.sub;
|
|
37
42
|
}
|
|
38
43
|
/** Active use renews the idle window, but never extends the original admission's eight-hour bound. */
|
|
39
44
|
export function renewGatewaySession({ session, authorizedExpiresAt, now = Date.now() }) {
|
|
@@ -161,3 +166,27 @@ export function signGatewayContext({ session, method, path, privateKey, keyId, n
|
|
|
161
166
|
const signed = `${header}.${payload}`;
|
|
162
167
|
return `${signed}.${sign(null, Buffer.from(signed), key).toString('base64url')}`;
|
|
163
168
|
}
|
|
169
|
+
/** Signs a proof only for the exact context this call just minted from its authenticated session. */
|
|
170
|
+
export function signGatewayRequest(input) {
|
|
171
|
+
const requestContext = signGatewayContext(input);
|
|
172
|
+
const authentication = input.session.authentication;
|
|
173
|
+
if (!authentication || authentication.method !== 'auth0_user' || authentication.sub !== input.session.ownerId) {
|
|
174
|
+
return { requestContext };
|
|
175
|
+
}
|
|
176
|
+
// This is our own just-signed payload, never a caller-supplied context token.
|
|
177
|
+
const context = artifactRequestContextSchema.parse(JSON.parse(Buffer.from(requestContext.split('.')[1], 'base64url').toString('utf8')));
|
|
178
|
+
const header = Buffer.from(JSON.stringify({ alg: 'EdDSA', typ: 'JWT', kid: input.keyId })).toString('base64url');
|
|
179
|
+
const payload = Buffer.from(JSON.stringify({
|
|
180
|
+
purpose: 'artifact-request-authentication', v: 1, iss: context.iss, aud: context.aud,
|
|
181
|
+
authenticationMethod: authentication.method, sub: authentication.sub,
|
|
182
|
+
contextJti: context.jti, contextSha256: createHash('sha256').update(requestContext).digest('hex'),
|
|
183
|
+
projectId: context.projectId, environment: context.environment, method: context.method,
|
|
184
|
+
pathSha256: createHash('sha256').update(context.path).digest('hex'),
|
|
185
|
+
iat: context.iat, exp: context.exp,
|
|
186
|
+
})).toString('base64url');
|
|
187
|
+
const unsigned = `${header}.${payload}`;
|
|
188
|
+
const requestAuthentication = `${unsigned}.${sign(null, Buffer.from(unsigned), createPrivateKey(input.privateKey)).toString('base64url')}`;
|
|
189
|
+
if (requestAuthentication.length > ARTIFACT_REQUEST_AUTHENTICATION_MAX_LENGTH)
|
|
190
|
+
throw new Error('Artifact authentication proof exceeds the header limit');
|
|
191
|
+
return { requestContext, requestAuthentication };
|
|
192
|
+
}
|
package/dist/gateway-main.js
CHANGED
|
@@ -10,6 +10,7 @@ import { RedisTunnelOwnership, RedisTunnelRelay } from './tunnel/redis.js';
|
|
|
10
10
|
import { ArtifactRequestAdmission } from './gateway/admission.js';
|
|
11
11
|
import { GatewayControlAdmission } from './gateway/control-admission.js';
|
|
12
12
|
import { createIngressRuntime, IngressSecretsError } from './gateway/ingress-runtime.js';
|
|
13
|
+
import { DatadogStartupError, waitForDatadogStartup } from './gateway/datadog-startup.js';
|
|
13
14
|
function required(name) {
|
|
14
15
|
const value = process.env[name];
|
|
15
16
|
if (!value)
|
|
@@ -20,6 +21,7 @@ function required(name) {
|
|
|
20
21
|
async function main() {
|
|
21
22
|
if (process.env.ARTIFACT_NEXT_ENABLED !== 'true')
|
|
22
23
|
throw new Error('Artifact Next gateway is disabled');
|
|
24
|
+
await waitForDatadogStartup({ env: process.env });
|
|
23
25
|
const redisUrl = required('ARTIFACT_NEXT_REDIS_URL');
|
|
24
26
|
if (new URL(redisUrl).protocol !== 'rediss:')
|
|
25
27
|
throw new Error('Artifact tunnel routing requires private Redis with TLS');
|
|
@@ -135,9 +137,9 @@ async function main() {
|
|
|
135
137
|
process.once('SIGINT', () => { void stop(); });
|
|
136
138
|
}
|
|
137
139
|
void main().catch((error) => {
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
const reason = error instanceof IngressSecretsError ? error.message : undefined;
|
|
140
|
+
// These startup refusals contain only fixed descriptions and configuration names.
|
|
141
|
+
// Other errors (Redis, URLs) may carry connection strings.
|
|
142
|
+
const reason = error instanceof IngressSecretsError || error instanceof DatadogStartupError ? error.message : undefined;
|
|
141
143
|
process.stderr.write(`${JSON.stringify({ event: 'artifact_gateway.start_failed', ...(reason ? { reason } : {}) })}\n`);
|
|
142
144
|
process.exit(1);
|
|
143
145
|
});
|
package/dist/next-build.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare function resolveNextRepositoryId({ spec, repository, baseUrl, tok
|
|
|
15
15
|
baseUrl: string;
|
|
16
16
|
token: string;
|
|
17
17
|
}): Promise<string>;
|
|
18
|
-
export declare function submitNextBuild({ root, sourceRoot, manifest, projectId, baseUrl, getToken, provenance, repository, repositoryId, channel, out }: {
|
|
18
|
+
export declare function submitNextBuild({ root, sourceRoot, manifest, projectId, baseUrl, getToken, provenance, repository, repositoryId, sourceEnvironment, sourceToken, channel, out }: {
|
|
19
19
|
root: string;
|
|
20
20
|
sourceRoot: string;
|
|
21
21
|
manifest: ArtifactStudioNextManifest;
|
|
@@ -25,6 +25,8 @@ export declare function submitNextBuild({ root, sourceRoot, manifest, projectId,
|
|
|
25
25
|
provenance: SourceProvenance;
|
|
26
26
|
repository: string;
|
|
27
27
|
repositoryId?: string;
|
|
28
|
+
sourceEnvironment?: string;
|
|
29
|
+
sourceToken?: string;
|
|
28
30
|
channel: 'active' | 'preview';
|
|
29
31
|
out?: string;
|
|
30
32
|
}): Promise<number>;
|
package/dist/next-build.js
CHANGED
|
@@ -58,16 +58,16 @@ export async function resolveNextRepositoryId({ spec, repository, baseUrl, token
|
|
|
58
58
|
return match[1];
|
|
59
59
|
}
|
|
60
60
|
catch { /* Opaque labels/SSH origins are valid preview provenance but cannot attest a release. */ }
|
|
61
|
-
throw new Error('Official Next.js releases require a Git Service repository in
|
|
61
|
+
throw new Error('Official Next.js releases require a Git Service repository in the selected source environment. Use --source-env <env> when its repository lives in another environment. Push the reviewed revision, then run artifact deploy --repo <namespace>/<name> --ref main --env <env> (or --ref staging for a staging environment).');
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* A verified repository build names a commit and the application's directory in it;
|
|
65
65
|
* anything else uploads the committed source snapshot. The repository root is omitted
|
|
66
66
|
* so the request stays valid for Atlas versions that predate `sourceRoot`.
|
|
67
67
|
*/
|
|
68
|
-
function nextBuildRequestBody({ manifest, files, commit, provenance, repository, repositoryId, sourceRoot, channel }) {
|
|
68
|
+
function nextBuildRequestBody({ manifest, files, commit, provenance, repository, repositoryId, sourceEnvironment, sourceRoot, channel }) {
|
|
69
69
|
if (repositoryId)
|
|
70
|
-
return { repositoryId, commit, ...(sourceRoot === '.' ? {} : { sourceRoot }), idempotencyKey: randomUUID(), channel };
|
|
70
|
+
return { repositoryId, ...(sourceEnvironment ? { sourceEnvironment } : {}), commit, ...(sourceRoot === '.' ? {} : { sourceRoot }), idempotencyKey: randomUUID(), channel };
|
|
71
71
|
return {
|
|
72
72
|
manifest,
|
|
73
73
|
files: files.map(({ path, content, encoding }) => ({ path, content, encoding: encoding ?? 'utf8' })),
|
|
@@ -75,7 +75,7 @@ function nextBuildRequestBody({ manifest, files, commit, provenance, repository,
|
|
|
75
75
|
idempotencyKey: randomUUID(), channel,
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
|
-
export async function submitNextBuild({ root, sourceRoot, manifest, projectId, baseUrl, getToken, provenance, repository, repositoryId, channel, out }) {
|
|
78
|
+
export async function submitNextBuild({ root, sourceRoot, manifest, projectId, baseUrl, getToken, provenance, repository, repositoryId, sourceEnvironment, sourceToken, channel, out }) {
|
|
79
79
|
if (!provenance.gitCommit || !/^[a-f0-9]{40}$/i.test(provenance.gitCommit)) {
|
|
80
80
|
throw new Error('Commit the application source before requesting a hosted build. Use artifact dev for uncommitted local development.');
|
|
81
81
|
}
|
|
@@ -84,8 +84,12 @@ export async function submitNextBuild({ root, sourceRoot, manifest, projectId, b
|
|
|
84
84
|
if (channel === 'active' && !repositoryId)
|
|
85
85
|
throw new Error('An official release requires a verified Git Service repository.');
|
|
86
86
|
const path = `/api/artifact-studio/projects/${encodeURIComponent(projectId)}/next`;
|
|
87
|
-
const body = nextBuildRequestBody({ manifest, files: source.files, commit: provenance.gitCommit, provenance, repository, repositoryId, sourceRoot, channel });
|
|
88
|
-
|
|
87
|
+
const body = nextBuildRequestBody({ manifest, files: source.files, commit: provenance.gitCommit, provenance, repository, repositoryId, sourceEnvironment, sourceRoot, channel });
|
|
88
|
+
if (sourceEnvironment && !sourceToken)
|
|
89
|
+
throw new Error('Cross-environment builds require a separately authenticated source session.');
|
|
90
|
+
const response = operationSchema.parse(await postJson({ baseUrl, token: await getToken(), path: `${path}/builds${repositoryId ? '/from-repository' : ''}`, body, ...(sourceEnvironment ? { headers: { 'x-sequence-source-authorization': `Bearer ${sourceToken}` } } : {}) }));
|
|
91
|
+
if (sourceEnvironment)
|
|
92
|
+
console.log(`[seq-studio] source: ${sourceEnvironment} / ${repository} -> target: ${baseUrl}`);
|
|
89
93
|
const directory = repositoryId && sourceRoot !== '.' ? ` (${sourceRoot})` : '';
|
|
90
94
|
console.log(`[seq-studio] Atlas build ${response.operation.id} (${channel}) from ${provenance.gitCommit.slice(0, 12)}${directory}`);
|
|
91
95
|
console.log(`${baseUrl}/artifact-studio/projects/${projectId}`);
|
package/dist/sdk.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface SequenceApiFetchResponse<TBody = unknown> {
|
|
|
44
44
|
headers?: Record<string, string>;
|
|
45
45
|
}
|
|
46
46
|
export interface SequenceApiUploadOptions {
|
|
47
|
+
/** Multipart binary field name. Defaults to file. */
|
|
48
|
+
fieldName?: string;
|
|
47
49
|
timeoutMs?: number;
|
|
48
50
|
/**
|
|
49
51
|
* HTTP method for the multipart request. Defaults to POST; PUT is allowed
|