@sequenceholdings/artifact-studio 0.2.6 → 0.3.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/VERSION-PIN.md +8 -1
- package/dist/api.d.ts +11 -1
- package/dist/api.js +26 -9
- package/dist/build.d.ts +2 -2
- package/dist/build.js +4 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +105 -10
- package/dist/dev-link.d.ts +1 -1
- package/dist/dev-link.js +2 -1
- package/dist/gateway/admission.d.ts +55 -0
- package/dist/gateway/admission.js +132 -0
- package/dist/gateway/control.d.ts +91 -0
- package/dist/gateway/control.js +113 -0
- package/dist/gateway/hosted-proxy.d.ts +36 -0
- package/dist/gateway/hosted-proxy.js +347 -0
- package/dist/gateway/release-admission.d.ts +23 -0
- package/dist/gateway/release-admission.js +259 -0
- package/dist/gateway/server.d.ts +29 -0
- package/dist/gateway/server.js +298 -0
- package/dist/gateway/session.d.ts +71 -0
- package/dist/gateway/session.js +143 -0
- package/dist/gateway/usage.d.ts +37 -0
- package/dist/gateway/usage.js +46 -0
- package/dist/gateway-main.d.ts +1 -0
- package/dist/gateway-main.js +84 -0
- package/dist/git-clone.d.ts +23 -1
- package/dist/git-clone.js +32 -15
- package/dist/manifest-next.d.ts +217 -0
- package/dist/manifest-next.js +120 -0
- package/dist/manifest-static.d.ts +76 -0
- package/dist/manifest-static.js +124 -0
- package/dist/manifest.d.ts +118 -13
- package/dist/manifest.js +12 -119
- package/dist/next/bridge.d.ts +47 -0
- package/dist/next/bridge.js +229 -0
- package/dist/next/browser-logs.d.ts +7 -0
- package/dist/next/browser-logs.js +51 -0
- package/dist/next/browser.d.ts +6 -0
- package/dist/next/browser.js +57 -0
- package/dist/next/protocol.d.ts +73 -0
- package/dist/next/protocol.js +56 -0
- package/dist/next/release.d.ts +15 -0
- package/dist/next/release.js +46 -0
- package/dist/next/requests.d.ts +15 -0
- package/dist/next/requests.js +59 -0
- package/dist/next/routes.d.ts +4 -0
- package/dist/next/routes.js +21 -0
- package/dist/next/streams.d.ts +16 -0
- package/dist/next/streams.js +132 -0
- package/dist/next/theme.d.ts +10 -0
- package/dist/next/theme.js +45 -0
- package/dist/next/transport.d.ts +15 -0
- package/dist/next/transport.js +13 -0
- package/dist/next-build.d.ts +29 -0
- package/dist/next-build.js +129 -0
- package/dist/next-cache/adapter-runtime.d.ts +51 -0
- package/dist/next-cache/adapter-runtime.js +73 -0
- package/dist/next-cache/component-handler.d.ts +3 -0
- package/dist/next-cache/component-handler.js +10 -0
- package/dist/next-cache/components.d.ts +20 -0
- package/dist/next-cache/components.js +113 -0
- package/dist/next-cache/config.d.ts +9 -0
- package/dist/next-cache/config.js +78 -0
- package/dist/next-cache/incremental-handler.d.ts +18 -0
- package/dist/next-cache/incremental-handler.js +17 -0
- package/dist/next-cache/incremental.d.ts +27 -0
- package/dist/next-cache/incremental.js +99 -0
- package/dist/next-cache/index.d.ts +18 -0
- package/dist/next-cache/index.js +16 -0
- package/dist/next-cache/memory-store.d.ts +27 -0
- package/dist/next-cache/memory-store.js +118 -0
- package/dist/next-cache/redis-scripts.d.ts +3 -0
- package/dist/next-cache/redis-scripts.js +85 -0
- package/dist/next-cache/redis-store.d.ts +18 -0
- package/dist/next-cache/redis-store.js +56 -0
- package/dist/next-cache/runtime.d.ts +29 -0
- package/dist/next-cache/runtime.js +63 -0
- package/dist/next-cache/serialization.d.ts +39 -0
- package/dist/next-cache/serialization.js +333 -0
- package/dist/next-cache/store.d.ts +54 -0
- package/dist/next-cache/store.js +45 -0
- package/dist/next-cache/valkey-transport.d.ts +52 -0
- package/dist/next-cache/valkey-transport.js +189 -0
- package/dist/next-dev-control.d.ts +48 -0
- package/dist/next-dev-control.js +63 -0
- package/dist/next-dev-process.d.ts +17 -0
- package/dist/next-dev-process.js +110 -0
- package/dist/next-dev.d.ts +12 -0
- package/dist/next-dev.js +267 -0
- package/dist/next-image-config.d.ts +9 -0
- package/dist/next-image-config.js +101 -0
- package/dist/next-project.d.ts +22 -0
- package/dist/next-project.js +64 -0
- package/dist/next-public-fonts-local.d.ts +7 -0
- package/dist/next-public-fonts-local.js +64 -0
- package/dist/next-public-fonts.d.ts +13 -0
- package/dist/next-public-fonts.js +49 -0
- package/dist/next-source-hash.d.ts +14 -0
- package/dist/next-source-hash.js +18 -0
- package/dist/next-source.d.ts +35 -0
- package/dist/next-source.js +155 -0
- package/dist/next.d.ts +9 -0
- package/dist/next.js +5 -0
- package/dist/project.d.ts +3 -0
- package/dist/project.js +9 -1
- package/dist/runtime-contracts.d.ts +229 -0
- package/dist/runtime-contracts.js +224 -0
- package/dist/sdk.d.ts +2 -2
- package/dist/server/broker-contract.d.ts +68 -0
- package/dist/server/broker-contract.js +33 -0
- package/dist/server/client.d.ts +67 -0
- package/dist/server/client.js +159 -0
- package/dist/server/logger.d.ts +26 -0
- package/dist/server/logger.js +24 -0
- package/dist/server/release-forms.d.ts +13 -0
- package/dist/server/release-forms.js +124 -0
- package/dist/server/request-context.d.ts +74 -0
- package/dist/server/request-context.js +103 -0
- package/dist/server/runtime-config.d.ts +53 -0
- package/dist/server/runtime-config.js +64 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +4 -0
- package/dist/templates/nextjs/.artifactignore +14 -0
- package/dist/templates/nextjs/README.md +151 -0
- package/dist/templates/nextjs/app/about/page.tsx +26 -0
- package/dist/templates/nextjs/app/actions.ts +18 -0
- package/dist/templates/nextjs/app/api/health/route.ts +6 -0
- package/dist/templates/nextjs/app/components/counter.tsx +19 -0
- package/dist/templates/nextjs/app/components/greeting-form.tsx +38 -0
- package/dist/templates/nextjs/app/globals.css +4 -0
- package/dist/templates/nextjs/app/layout.tsx +27 -0
- package/dist/templates/nextjs/app/page.tsx +51 -0
- package/dist/templates/nextjs/app/providers.tsx +107 -0
- package/dist/templates/nextjs/artifact.bundle.yml +55 -0
- package/dist/templates/nextjs/next.config.mjs +27 -0
- package/dist/templates/nextjs/package.json +32 -0
- package/dist/templates/nextjs/pnpm-workspace.yaml +8 -0
- package/dist/templates/nextjs/postcss.config.mjs +5 -0
- package/dist/templates/nextjs/tsconfig.json +20 -0
- package/dist/templates/react-vite/package.json +1 -1
- package/dist/tunnel/broker.d.ts +75 -0
- package/dist/tunnel/broker.js +451 -0
- package/dist/tunnel/client.d.ts +22 -0
- package/dist/tunnel/client.js +177 -0
- package/dist/tunnel/index.d.ts +6 -0
- package/dist/tunnel/index.js +4 -0
- package/dist/tunnel/io.d.ts +33 -0
- package/dist/tunnel/io.js +108 -0
- package/dist/tunnel/peer.d.ts +25 -0
- package/dist/tunnel/peer.js +147 -0
- package/dist/tunnel/protocol.d.ts +69 -0
- package/dist/tunnel/protocol.js +196 -0
- package/dist/tunnel/redis-ownership.d.ts +38 -0
- package/dist/tunnel/redis-ownership.js +100 -0
- package/dist/tunnel/redis-socket.d.ts +13 -0
- package/dist/tunnel/redis-socket.js +48 -0
- package/dist/tunnel/redis-types.d.ts +70 -0
- package/dist/tunnel/redis-types.js +9 -0
- package/dist/tunnel/redis.d.ts +53 -0
- package/dist/tunnel/redis.js +391 -0
- package/dist/tunnel/transport.d.ts +17 -0
- package/dist/tunnel/transport.js +1 -0
- package/package.json +67 -4
- package/templates/nextjs/.artifactignore +14 -0
- package/templates/nextjs/README.md +151 -0
- package/templates/nextjs/app/about/page.tsx +26 -0
- package/templates/nextjs/app/actions.ts +18 -0
- package/templates/nextjs/app/api/health/route.ts +6 -0
- package/templates/nextjs/app/components/counter.tsx +19 -0
- package/templates/nextjs/app/components/greeting-form.tsx +38 -0
- package/templates/nextjs/app/globals.css +4 -0
- package/templates/nextjs/app/layout.tsx +27 -0
- package/templates/nextjs/app/page.tsx +51 -0
- package/templates/nextjs/app/providers.tsx +107 -0
- package/templates/nextjs/artifact.bundle.yml +55 -0
- package/templates/nextjs/next.config.mjs +27 -0
- package/templates/nextjs/package.json +32 -0
- package/templates/nextjs/pnpm-workspace.yaml +8 -0
- package/templates/nextjs/postcss.config.mjs +5 -0
- package/templates/nextjs/tsconfig.json +20 -0
- package/templates/react-vite/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { TunnelLease } from '../tunnel/index.js';
|
|
3
|
+
import type { GatewaySession } from './session.js';
|
|
4
|
+
import type { GatewayUsageEvent } from './usage.js';
|
|
5
|
+
export declare const gatewayTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
6
|
+
kind: z.ZodLiteral<"development">;
|
|
7
|
+
sessionId: z.ZodString;
|
|
8
|
+
epoch: z.ZodNumber;
|
|
9
|
+
projectId: z.ZodString;
|
|
10
|
+
ownerId: z.ZodString;
|
|
11
|
+
environment: z.ZodString;
|
|
12
|
+
publicOrigin: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
13
|
+
expiresAt: z.ZodNumber;
|
|
14
|
+
maxExpiresAt: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
capabilities: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
16
|
+
serviceAccountId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
17
|
+
lattice: z.ZodOptional<z.ZodObject<{
|
|
18
|
+
runId: z.ZodNullable<z.ZodString>;
|
|
19
|
+
nodeId: z.ZodNullable<z.ZodString>;
|
|
20
|
+
stepId: z.ZodNullable<z.ZodString>;
|
|
21
|
+
processId: z.ZodNullable<z.ZodString>;
|
|
22
|
+
}, z.core.$strict>>;
|
|
23
|
+
taskRoutes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
24
|
+
manifest: z.ZodOptional<z.ZodObject<{
|
|
25
|
+
server: z.ZodObject<{
|
|
26
|
+
resources: z.ZodObject<{
|
|
27
|
+
concurrency: z.ZodNumber;
|
|
28
|
+
max_instances: z.ZodNumber;
|
|
29
|
+
timeout_seconds: z.ZodNumber;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
}, z.core.$strip>;
|
|
32
|
+
}, z.core.$strip>>;
|
|
33
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
34
|
+
kind: z.ZodLiteral<"hosted">;
|
|
35
|
+
deploymentId: z.ZodString;
|
|
36
|
+
frameworkBuildId: z.ZodString;
|
|
37
|
+
upstreamUrl: z.ZodString;
|
|
38
|
+
upstreamAudience: z.ZodOptional<z.ZodString>;
|
|
39
|
+
active: z.ZodBoolean;
|
|
40
|
+
published: z.ZodOptional<z.ZodBoolean>;
|
|
41
|
+
projectId: z.ZodString;
|
|
42
|
+
ownerId: z.ZodString;
|
|
43
|
+
environment: z.ZodString;
|
|
44
|
+
publicOrigin: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
45
|
+
expiresAt: z.ZodNumber;
|
|
46
|
+
maxExpiresAt: z.ZodOptional<z.ZodNumber>;
|
|
47
|
+
capabilities: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
48
|
+
serviceAccountId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
49
|
+
lattice: z.ZodOptional<z.ZodObject<{
|
|
50
|
+
runId: z.ZodNullable<z.ZodString>;
|
|
51
|
+
nodeId: z.ZodNullable<z.ZodString>;
|
|
52
|
+
stepId: z.ZodNullable<z.ZodString>;
|
|
53
|
+
processId: z.ZodNullable<z.ZodString>;
|
|
54
|
+
}, z.core.$strict>>;
|
|
55
|
+
taskRoutes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
56
|
+
manifest: z.ZodOptional<z.ZodObject<{
|
|
57
|
+
server: z.ZodObject<{
|
|
58
|
+
resources: z.ZodObject<{
|
|
59
|
+
concurrency: z.ZodNumber;
|
|
60
|
+
max_instances: z.ZodNumber;
|
|
61
|
+
timeout_seconds: z.ZodNumber;
|
|
62
|
+
}, z.core.$strip>;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
}, z.core.$strip>>;
|
|
65
|
+
}, z.core.$strip>], "kind">;
|
|
66
|
+
export type GatewayTarget = z.infer<typeof gatewayTargetSchema>;
|
|
67
|
+
export declare class GatewayControlError extends Error {
|
|
68
|
+
readonly status: number;
|
|
69
|
+
constructor(status: number);
|
|
70
|
+
}
|
|
71
|
+
export declare class ArtifactGatewayControl {
|
|
72
|
+
private readonly options;
|
|
73
|
+
private readonly origin;
|
|
74
|
+
constructor(options: {
|
|
75
|
+
url: string;
|
|
76
|
+
token: string;
|
|
77
|
+
fetch?: typeof fetch;
|
|
78
|
+
});
|
|
79
|
+
private call;
|
|
80
|
+
usage(event: GatewayUsageEvent): Promise<void>;
|
|
81
|
+
bootstrap(ticket: string): Promise<{
|
|
82
|
+
session: GatewaySession;
|
|
83
|
+
target: GatewayTarget;
|
|
84
|
+
}>;
|
|
85
|
+
authorize(session: GatewaySession): Promise<GatewayTarget>;
|
|
86
|
+
tunnel({ token, sessionId, epoch }: {
|
|
87
|
+
token: string;
|
|
88
|
+
sessionId: string;
|
|
89
|
+
epoch: number;
|
|
90
|
+
}): Promise<TunnelLease>;
|
|
91
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { normalizePublicOrigin } from '../tunnel/protocol.js';
|
|
3
|
+
import { artifactRequestContextSchema } from '../server/request-context.js';
|
|
4
|
+
import { artifactNextFrameworkBuildIdSchema } from '../runtime-contracts.js';
|
|
5
|
+
const id = z.string().min(1).max(255);
|
|
6
|
+
const base = {
|
|
7
|
+
projectId: id, ownerId: id, environment: id, publicOrigin: z.string().transform(normalizePublicOrigin),
|
|
8
|
+
expiresAt: z.number().int().positive(),
|
|
9
|
+
maxExpiresAt: z.number().int().positive().optional(),
|
|
10
|
+
capabilities: z.array(id).max(256).default([]), serviceAccountId: id.nullable().default(null),
|
|
11
|
+
lattice: artifactRequestContextSchema.shape.lattice.optional(),
|
|
12
|
+
taskRoutes: artifactRequestContextSchema.shape.taskRoutes,
|
|
13
|
+
manifest: z.object({ server: z.object({ resources: z.object({
|
|
14
|
+
concurrency: z.number().int().min(1).max(80), max_instances: z.number().int().min(1).max(100), timeout_seconds: z.number().int().min(1).max(300),
|
|
15
|
+
}) }) }).optional(),
|
|
16
|
+
};
|
|
17
|
+
export const gatewayTargetSchema = z.discriminatedUnion('kind', [
|
|
18
|
+
z.object({ ...base, kind: z.literal('development'), sessionId: id, epoch: z.number().int().positive() }),
|
|
19
|
+
z.object({ ...base, kind: z.literal('hosted'), deploymentId: id, frameworkBuildId: artifactNextFrameworkBuildIdSchema, upstreamUrl: z.string().url(), upstreamAudience: z.string().url().optional(), active: z.boolean(), published: z.boolean().optional() }),
|
|
20
|
+
]);
|
|
21
|
+
const bootstrapSchema = z.object({ parentOrigin: z.string().transform(normalizePublicOrigin), channelId: id, nonce: z.string().min(16).max(255) });
|
|
22
|
+
export class GatewayControlError extends Error {
|
|
23
|
+
status;
|
|
24
|
+
constructor(status) {
|
|
25
|
+
super('Artifact control plane request failed');
|
|
26
|
+
this.status = status;
|
|
27
|
+
this.name = 'GatewayControlError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class ArtifactGatewayControl {
|
|
31
|
+
options;
|
|
32
|
+
origin;
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.options = options;
|
|
35
|
+
this.origin = normalizePublicOrigin(options.url);
|
|
36
|
+
if (options.token.length < 32)
|
|
37
|
+
throw new Error('Artifact control plane token requires at least 32 characters');
|
|
38
|
+
}
|
|
39
|
+
async call(path, body, timeoutMs = 10_000) {
|
|
40
|
+
const response = await (this.options.fetch ?? fetch)(`${this.origin}/api/artifact-studio/next/control/${path}`, {
|
|
41
|
+
method: 'POST', headers: { authorization: `Bearer ${this.options.token}`, 'content-type': 'application/json' },
|
|
42
|
+
body: JSON.stringify(body), signal: AbortSignal.timeout(timeoutMs), redirect: 'error',
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok)
|
|
45
|
+
throw new GatewayControlError(response.status);
|
|
46
|
+
const contentLength = Number(response.headers.get('content-length'));
|
|
47
|
+
if (contentLength > 128 * 1024) {
|
|
48
|
+
await response.body?.cancel();
|
|
49
|
+
throw new GatewayControlError(502);
|
|
50
|
+
}
|
|
51
|
+
const reader = response.body?.getReader();
|
|
52
|
+
if (!reader)
|
|
53
|
+
throw new GatewayControlError(502);
|
|
54
|
+
const chunks = [];
|
|
55
|
+
let length = 0;
|
|
56
|
+
try {
|
|
57
|
+
for (;;) {
|
|
58
|
+
const { done, value } = await reader.read();
|
|
59
|
+
if (done)
|
|
60
|
+
break;
|
|
61
|
+
length += value.length;
|
|
62
|
+
if (length > 128 * 1024)
|
|
63
|
+
throw new GatewayControlError(502);
|
|
64
|
+
chunks.push(value);
|
|
65
|
+
}
|
|
66
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await reader.cancel().catch(() => undefined);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async usage(event) {
|
|
73
|
+
await this.call('usage', event, 2_000);
|
|
74
|
+
}
|
|
75
|
+
async bootstrap(ticket) {
|
|
76
|
+
const value = await this.call('bootstrap', { ticket });
|
|
77
|
+
const target = gatewayTargetSchema.parse(value), bridge = bootstrapSchema.parse(value);
|
|
78
|
+
const session = {
|
|
79
|
+
version: 1, kind: target.kind, projectId: target.projectId, ownerId: target.ownerId,
|
|
80
|
+
environment: target.environment, publicOrigin: target.publicOrigin,
|
|
81
|
+
sessionId: target.kind === 'development' ? target.sessionId : null,
|
|
82
|
+
deploymentId: target.kind === 'hosted' ? target.deploymentId : null,
|
|
83
|
+
frameworkBuildId: target.kind === 'hosted' ? target.frameworkBuildId : null,
|
|
84
|
+
expiresAt: Math.min(target.expiresAt, target.maxExpiresAt ?? target.expiresAt, Date.now() + 8 * 60 * 60_000),
|
|
85
|
+
maxExpiresAt: Math.min(target.maxExpiresAt ?? (target.kind === 'development' ? target.expiresAt : Date.now() + 8 * 60 * 60_000), Date.now() + 8 * 60 * 60_000),
|
|
86
|
+
published: target.kind === 'hosted' && (target.published ?? target.active), bridge,
|
|
87
|
+
...(target.lattice ? { lattice: target.lattice } : {}),
|
|
88
|
+
...(target.taskRoutes ? { taskRoutes: target.taskRoutes } : {}),
|
|
89
|
+
};
|
|
90
|
+
return { session, target };
|
|
91
|
+
}
|
|
92
|
+
async authorize(session) {
|
|
93
|
+
const value = await this.call('session', {
|
|
94
|
+
...(session.sessionId ? { sessionId: session.sessionId } : { deploymentId: session.deploymentId }),
|
|
95
|
+
ownerId: session.ownerId, published: session.published,
|
|
96
|
+
...(session.lattice ? { lattice: session.lattice } : {}),
|
|
97
|
+
...(session.taskRoutes ? { taskRoutes: session.taskRoutes } : {}),
|
|
98
|
+
});
|
|
99
|
+
const target = gatewayTargetSchema.parse(value);
|
|
100
|
+
if (target.kind !== session.kind || target.projectId !== session.projectId || target.ownerId !== session.ownerId ||
|
|
101
|
+
target.environment !== session.environment || target.publicOrigin !== session.publicOrigin || target.expiresAt <= Date.now() ||
|
|
102
|
+
(target.kind === 'development' ? target.sessionId !== session.sessionId : target.deploymentId !== session.deploymentId || target.frameworkBuildId !== session.frameworkBuildId))
|
|
103
|
+
throw new GatewayControlError(403);
|
|
104
|
+
return target;
|
|
105
|
+
}
|
|
106
|
+
async tunnel({ token, sessionId, epoch }) {
|
|
107
|
+
const value = await this.call('tunnel', { token, sessionId, epoch });
|
|
108
|
+
const parsed = z.object({ sessionId: id, epoch: z.number().int().positive(), ownerId: id, projectId: id, environment: id, expiresAt: z.number().int().positive(), publicOrigin: z.string().transform(normalizePublicOrigin) }).parse(value);
|
|
109
|
+
if (parsed.sessionId !== sessionId || parsed.epoch !== epoch || parsed.expiresAt <= Date.now())
|
|
110
|
+
throw new GatewayControlError(403);
|
|
111
|
+
return parsed;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
|
+
export declare const HOSTED_ARTIFACT_PROXY_LIMITS: Readonly<{
|
|
3
|
+
maxBodyBytes: number;
|
|
4
|
+
timeoutMs: 60000;
|
|
5
|
+
maxTimeoutMs: 300000;
|
|
6
|
+
}>;
|
|
7
|
+
export interface HostedArtifactProxyOptions {
|
|
8
|
+
request: IncomingMessage;
|
|
9
|
+
response: ServerResponse;
|
|
10
|
+
target: {
|
|
11
|
+
upstreamUrl: string;
|
|
12
|
+
/** Canonical service URL from the same provider record as the revision URL. */
|
|
13
|
+
upstreamAudience?: string;
|
|
14
|
+
publicOrigin: string;
|
|
15
|
+
projectId: string;
|
|
16
|
+
deploymentId: string;
|
|
17
|
+
};
|
|
18
|
+
requestContext: string;
|
|
19
|
+
/** Admission may inspect a native form prefix and replay it without buffering the body. */
|
|
20
|
+
body?: AsyncIterable<Uint8Array>;
|
|
21
|
+
identityTokenProvider?: (input: {
|
|
22
|
+
audience: string;
|
|
23
|
+
signal: AbortSignal;
|
|
24
|
+
}) => Promise<string>;
|
|
25
|
+
/** Injectable transport for isolated tests; the deployment URL is still validated first. */
|
|
26
|
+
resolveUpstream?: (input: {
|
|
27
|
+
url: URL;
|
|
28
|
+
}) => URL;
|
|
29
|
+
/** Approved request timeouts may exceed the default up to the platform cap. */
|
|
30
|
+
limits?: {
|
|
31
|
+
maxBodyBytes?: number;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** HTTP-only streaming hop. Authentication, CSRF and release admission happen before calling this. */
|
|
36
|
+
export declare function proxyHostedArtifact(options: HostedArtifactProxyOptions): Promise<void>;
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { request as httpRequest, validateHeaderValue } from 'node:http';
|
|
2
|
+
import { request as httpsRequest } from 'node:https';
|
|
3
|
+
import { Duplex, Readable, Transform } from 'node:stream';
|
|
4
|
+
import { createBrotliDecompress, createGunzip, createInflate } from 'node:zlib';
|
|
5
|
+
import { GoogleAuth } from 'google-auth-library';
|
|
6
|
+
import { assertSafeRequest, normalizePublicOrigin, sanitizeRequestHeaders, sanitizeResponseHeaders } from '../tunnel/protocol.js';
|
|
7
|
+
import { createReleaseFormTransform } from '../server/release-forms.js';
|
|
8
|
+
export const HOSTED_ARTIFACT_PROXY_LIMITS = Object.freeze({ maxBodyBytes: 32 * 1024 * 1024, timeoutMs: 60_000, maxTimeoutMs: 300_000 });
|
|
9
|
+
const googleAuth = new GoogleAuth();
|
|
10
|
+
const identityClients = new Map();
|
|
11
|
+
async function defaultIdentityTokenProvider({ audience }) {
|
|
12
|
+
let pending = identityClients.get(audience);
|
|
13
|
+
if (!pending) {
|
|
14
|
+
if (identityClients.size >= 256) {
|
|
15
|
+
const oldest = identityClients.keys().next().value;
|
|
16
|
+
if (oldest)
|
|
17
|
+
identityClients.delete(oldest);
|
|
18
|
+
}
|
|
19
|
+
pending = googleAuth.getIdTokenClient(audience);
|
|
20
|
+
identityClients.set(audience, pending);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const client = await pending;
|
|
24
|
+
const headers = await client.getRequestHeaders();
|
|
25
|
+
const authorization = headers.get('authorization');
|
|
26
|
+
if (!authorization?.startsWith('Bearer '))
|
|
27
|
+
throw new Error('Missing platform identity token');
|
|
28
|
+
return authorization.slice('Bearer '.length);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
identityClients.delete(audience);
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function cloudRunOrigin(value) {
|
|
36
|
+
const url = new URL(value);
|
|
37
|
+
if (!/^https:\/\/[a-z0-9.-]+\.run\.app\/?$/.test(value) || url.protocol !== 'https:' ||
|
|
38
|
+
url.username || url.password || url.port || url.pathname !== '/' || url.search || url.hash ||
|
|
39
|
+
!url.hostname.split('.').every((part) => /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(part)))
|
|
40
|
+
throw new Error('Invalid hosted deployment origin');
|
|
41
|
+
return url;
|
|
42
|
+
}
|
|
43
|
+
function stripCloudHeaders(headers) {
|
|
44
|
+
for (const name of Object.keys(headers)) {
|
|
45
|
+
if (name === 'x-serverless-authorization' || name.startsWith('x-goog-'))
|
|
46
|
+
delete headers[name];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function rewriteLocation({ location, upstream, publicOrigin }) {
|
|
50
|
+
if (!/^(?:https?:)?\/\//i.test(location))
|
|
51
|
+
return location;
|
|
52
|
+
const url = new URL(location, upstream);
|
|
53
|
+
return url.origin === upstream.origin ? `${publicOrigin}${url.pathname}${url.search}${url.hash}` : location;
|
|
54
|
+
}
|
|
55
|
+
function mergeFramePolicy({ application, trusted }) {
|
|
56
|
+
const policies = Array.isArray(application) ? application : [application];
|
|
57
|
+
const frames = Array.isArray(trusted) ? trusted : [trusted];
|
|
58
|
+
return [...policies.map((policy) => policy.split(';')
|
|
59
|
+
.filter((directive) => directive.trim().split(/\s/, 1)[0].toLowerCase() !== 'frame-ancestors')
|
|
60
|
+
.map((directive) => directive.trim()).filter(Boolean).join('; ')).filter(Boolean), ...frames];
|
|
61
|
+
}
|
|
62
|
+
function enforceBrowserCachePolicy({ request, response }) {
|
|
63
|
+
const path = (request.url ?? '/').split('?', 1)[0];
|
|
64
|
+
let staticAsset = false;
|
|
65
|
+
try {
|
|
66
|
+
const segments = path.slice(1).split('/').map((segment) => decodeURIComponent(segment));
|
|
67
|
+
staticAsset = path.startsWith('/_next/static/') && /\.(?:js|css)$/.test(path) &&
|
|
68
|
+
segments.every((segment) => segment !== '' && segment !== '.' && segment !== '..' && !/[\\/]/.test(segment));
|
|
69
|
+
}
|
|
70
|
+
catch { /* Malformed or ambiguous asset paths retain the default no-store policy. */ }
|
|
71
|
+
const contentType = response.getHeader('content-type');
|
|
72
|
+
const mediaType = typeof contentType === 'string' ? contentType.split(';', 1)[0].trim().toLowerCase() : undefined;
|
|
73
|
+
const assetType = path.endsWith('.css') ? mediaType === 'text/css' : mediaType === 'text/javascript' || mediaType === 'application/javascript';
|
|
74
|
+
const revalidateAsset = staticAsset && ['GET', 'HEAD'].includes(request.method ?? 'GET') &&
|
|
75
|
+
(response.statusCode === 200 || response.statusCode === 304) &&
|
|
76
|
+
(assetType || (response.statusCode === 304 && contentType === undefined));
|
|
77
|
+
// Every protected reuse must reach authorization. Retain only private static
|
|
78
|
+
// JS/CSS bytes and validators; app output may never opt into browser/CDN TTLs.
|
|
79
|
+
for (const name of response.getHeaderNames()) {
|
|
80
|
+
if (/(?:^|-)cdn-cache-control$/.test(name) || ['surrogate-control', 'x-accel-expires', 'expires', 'age'].includes(name)) {
|
|
81
|
+
response.removeHeader(name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
response.setHeader('cache-control', revalidateAsset ? 'private, no-cache' : 'private, no-store');
|
|
85
|
+
response.setHeader('cdn-cache-control', 'no-store');
|
|
86
|
+
response.setHeader('surrogate-control', 'no-store');
|
|
87
|
+
response.setHeader('pragma', 'no-cache');
|
|
88
|
+
response.setHeader('expires', '0');
|
|
89
|
+
}
|
|
90
|
+
/** HTTP-only streaming hop. Authentication, CSRF and release admission happen before calling this. */
|
|
91
|
+
export function proxyHostedArtifact(options) {
|
|
92
|
+
return new HostedExchange(options).run();
|
|
93
|
+
}
|
|
94
|
+
class HostedExchange {
|
|
95
|
+
options;
|
|
96
|
+
controller = new AbortController();
|
|
97
|
+
maximum;
|
|
98
|
+
timeoutMs;
|
|
99
|
+
timer;
|
|
100
|
+
upload;
|
|
101
|
+
outgoing;
|
|
102
|
+
incoming;
|
|
103
|
+
responseSource;
|
|
104
|
+
transforms = [];
|
|
105
|
+
stopped = false;
|
|
106
|
+
sentBytes = 0;
|
|
107
|
+
receivedBytes = 0;
|
|
108
|
+
resolve;
|
|
109
|
+
constructor(options) {
|
|
110
|
+
this.options = options;
|
|
111
|
+
this.maximum = this.boundLimit(options.limits?.maxBodyBytes, HOSTED_ARTIFACT_PROXY_LIMITS.maxBodyBytes);
|
|
112
|
+
this.timeoutMs = this.boundLimit(options.limits?.timeoutMs, HOSTED_ARTIFACT_PROXY_LIMITS.maxTimeoutMs, HOSTED_ARTIFACT_PROXY_LIMITS.timeoutMs);
|
|
113
|
+
}
|
|
114
|
+
boundLimit(value, maximum, fallback = maximum) {
|
|
115
|
+
return Number.isSafeInteger(value) && value !== undefined && value > 0 ? Math.min(value, maximum) : fallback;
|
|
116
|
+
}
|
|
117
|
+
run() {
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
this.resolve = resolve;
|
|
120
|
+
const { request, response } = this.options;
|
|
121
|
+
if (request.aborted || response.destroyed || response.writableEnded) {
|
|
122
|
+
this.finish();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
response.once('finish', this.finish);
|
|
126
|
+
response.once('close', this.finish);
|
|
127
|
+
response.once('error', this.finish);
|
|
128
|
+
request.once('aborted', this.finish);
|
|
129
|
+
request.once('error', this.finish);
|
|
130
|
+
this.timer = setTimeout(() => this.fail(504), this.timeoutMs);
|
|
131
|
+
this.timer.unref();
|
|
132
|
+
void this.prepare().catch(() => this.fail(502));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async prepare() {
|
|
136
|
+
const { request, response, target, requestContext } = this.options;
|
|
137
|
+
const upstream = cloudRunOrigin(target.upstreamUrl);
|
|
138
|
+
// Cloud Run revision tags select immutable transport destinations, but IAM
|
|
139
|
+
// requires the canonical service audience. Both are control-plane owned;
|
|
140
|
+
// never derive this from a browser host, header or query parameter.
|
|
141
|
+
const audience = target.upstreamAudience === undefined ? upstream : cloudRunOrigin(target.upstreamAudience);
|
|
142
|
+
const publicOrigin = normalizePublicOrigin(target.publicOrigin);
|
|
143
|
+
try {
|
|
144
|
+
assertSafeRequest(request.method ?? 'GET', request.url ?? '/');
|
|
145
|
+
validateHeaderValue('x-sequence-artifact-context', requestContext);
|
|
146
|
+
if (!requestContext || requestContext.length > 16384)
|
|
147
|
+
throw new Error('Invalid request context');
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
this.fail(400);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const length = request.headers['content-length'];
|
|
154
|
+
if (length !== undefined && (!/^(0|[1-9][0-9]*)$/.test(length) || Number(length) > this.maximum)) {
|
|
155
|
+
this.fail(413);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const headers = sanitizeRequestHeaders(request.headers, publicOrigin);
|
|
159
|
+
stripCloudHeaders(headers);
|
|
160
|
+
const provider = this.options.identityTokenProvider ?? defaultIdentityTokenProvider;
|
|
161
|
+
const token = await provider({ audience: audience.origin, signal: this.controller.signal });
|
|
162
|
+
if (this.stopped)
|
|
163
|
+
return;
|
|
164
|
+
if (!/^[A-Za-z0-9._~-]+$/.test(token) || token.length > 16384)
|
|
165
|
+
throw new Error('Invalid platform token');
|
|
166
|
+
headers.host = upstream.host;
|
|
167
|
+
// HTML forms receive release markers. Avoid compression when possible;
|
|
168
|
+
// runtimes that still compress HTML are decoded as a bounded stream below.
|
|
169
|
+
headers['accept-encoding'] = 'identity';
|
|
170
|
+
// Cloud Run removes this token's signature before passing it to user code.
|
|
171
|
+
// https://cloud.google.com/run/docs/authenticating/service-to-service
|
|
172
|
+
headers['x-serverless-authorization'] = `Bearer ${token}`;
|
|
173
|
+
headers['x-sequence-artifact-context'] = requestContext;
|
|
174
|
+
const destination = this.resolveDestination(upstream);
|
|
175
|
+
const transport = destination.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
176
|
+
this.outgoing = transport(destination, { method: request.method, path: request.url, headers, maxHeaderSize: 32 * 1024 }, (incoming) => this.receive({ incoming, upstream, publicOrigin }));
|
|
177
|
+
this.outgoing.on('error', () => this.fail(502));
|
|
178
|
+
this.outgoing.once('upgrade', (_incoming, socket) => { socket.destroy(); this.fail(502); });
|
|
179
|
+
this.outgoing.on('drain', () => { if (!this.stopped)
|
|
180
|
+
this.upload?.resume(); });
|
|
181
|
+
this.upload = this.options.body ? Readable.from(this.options.body, { highWaterMark: 64 * 1024, objectMode: false }) : request;
|
|
182
|
+
this.upload.on('data', this.sendChunk);
|
|
183
|
+
this.upload.once('end', () => this.outgoing?.end());
|
|
184
|
+
this.upload.once('error', () => this.fail(502));
|
|
185
|
+
response.on('drain', () => { if (!this.stopped)
|
|
186
|
+
this.responseSource?.resume(); });
|
|
187
|
+
}
|
|
188
|
+
resolveDestination(upstream) {
|
|
189
|
+
const destination = this.options.resolveUpstream?.({ url: upstream }) ?? upstream;
|
|
190
|
+
const injectedLoopback = destination.protocol === 'http:' && ['127.0.0.1', '[::1]'].includes(destination.hostname);
|
|
191
|
+
if ((destination.origin !== upstream.origin && !injectedLoopback) || destination.username || destination.password ||
|
|
192
|
+
destination.pathname !== '/' || destination.search || destination.hash)
|
|
193
|
+
throw new Error('Invalid hosted transport destination');
|
|
194
|
+
return destination;
|
|
195
|
+
}
|
|
196
|
+
sendChunk = (chunk) => {
|
|
197
|
+
if (this.stopped)
|
|
198
|
+
return;
|
|
199
|
+
this.sentBytes += chunk.byteLength;
|
|
200
|
+
if (this.sentBytes > this.maximum) {
|
|
201
|
+
this.fail(413);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!this.outgoing?.write(chunk))
|
|
205
|
+
this.upload?.pause();
|
|
206
|
+
};
|
|
207
|
+
receive({ incoming, upstream, publicOrigin }) {
|
|
208
|
+
this.incoming = incoming;
|
|
209
|
+
if (this.stopped) {
|
|
210
|
+
incoming.destroy();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const headers = sanitizeResponseHeaders(incoming.headers);
|
|
215
|
+
stripCloudHeaders(headers);
|
|
216
|
+
delete headers['x-frame-options'];
|
|
217
|
+
if (typeof headers.location === 'string')
|
|
218
|
+
headers.location = rewriteLocation({ location: headers.location, upstream, publicOrigin });
|
|
219
|
+
const response = this.options.response;
|
|
220
|
+
this.responseSource = this.prepareResponseSource({ incoming, headers, publicOrigin });
|
|
221
|
+
const framePolicy = response.getHeader('content-security-policy');
|
|
222
|
+
if (headers['content-security-policy'] && (typeof framePolicy === 'string' || Array.isArray(framePolicy))) {
|
|
223
|
+
headers['content-security-policy'] = mergeFramePolicy({ application: headers['content-security-policy'], trusted: framePolicy });
|
|
224
|
+
}
|
|
225
|
+
response.statusCode = incoming.statusCode ?? 502;
|
|
226
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
227
|
+
const existing = name === 'set-cookie' ? response.getHeader(name) : undefined;
|
|
228
|
+
if (typeof existing === 'string' || Array.isArray(existing)) {
|
|
229
|
+
// Rolling gateway leases and application cookies share the response,
|
|
230
|
+
// but application cookies were stripped of reserved names above.
|
|
231
|
+
response.setHeader(name, [...(Array.isArray(existing) ? existing : [existing]), ...(Array.isArray(value) ? value : [value])]);
|
|
232
|
+
}
|
|
233
|
+
else
|
|
234
|
+
response.setHeader(name, value);
|
|
235
|
+
}
|
|
236
|
+
enforceBrowserCachePolicy({ request: this.options.request, response });
|
|
237
|
+
this.responseSource.on('data', this.receiveChunk);
|
|
238
|
+
this.responseSource.once('end', () => response.end());
|
|
239
|
+
this.responseSource.once('error', () => this.fail(502));
|
|
240
|
+
incoming.once('error', () => this.fail(502));
|
|
241
|
+
incoming.once('aborted', () => this.fail(502));
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
this.fail(502);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
prepareResponseSource({ incoming, headers, publicOrigin }) {
|
|
248
|
+
const contentType = headers['content-type'];
|
|
249
|
+
if (typeof contentType !== 'string' || contentType.split(';', 1)[0].trim().toLowerCase() !== 'text/html')
|
|
250
|
+
return incoming;
|
|
251
|
+
const disposition = headers['content-disposition'];
|
|
252
|
+
if (typeof disposition === 'string' && /^attachment(?:;|$)/i.test(disposition.trim()))
|
|
253
|
+
return incoming;
|
|
254
|
+
if (/;\s*charset\s*=/i.test(contentType) && !/;\s*charset\s*=\s*"?utf-?8"?\s*(?:;|$)/i.test(contentType))
|
|
255
|
+
throw new Error('Unsupported HTML encoding');
|
|
256
|
+
const encoding = headers['content-encoding'];
|
|
257
|
+
for (const header of ['content-encoding', 'content-length', 'etag', 'content-md5', 'digest', 'content-digest', 'repr-digest', 'accept-ranges'])
|
|
258
|
+
delete headers[header];
|
|
259
|
+
if (this.options.request.method === 'HEAD' || incoming.statusCode === 204 || incoming.statusCode === 304)
|
|
260
|
+
return incoming;
|
|
261
|
+
if (incoming.statusCode === 206)
|
|
262
|
+
throw new Error('Partial HTML cannot receive release markers');
|
|
263
|
+
let source = incoming;
|
|
264
|
+
if (encoding && encoding !== 'identity') {
|
|
265
|
+
const decoder = encoding === 'gzip' ? createGunzip() : encoding === 'br' ? createBrotliDecompress() : encoding === 'deflate' ? createInflate() : undefined;
|
|
266
|
+
if (!decoder)
|
|
267
|
+
throw new Error('Unsupported HTML compression');
|
|
268
|
+
this.transforms.push(decoder);
|
|
269
|
+
source = source.pipe(decoder);
|
|
270
|
+
}
|
|
271
|
+
let decodedBytes = 0;
|
|
272
|
+
const maximum = this.maximum;
|
|
273
|
+
const limit = new Transform({ transform(chunk, _encoding, callback) {
|
|
274
|
+
decodedBytes += chunk.byteLength;
|
|
275
|
+
if (decodedBytes > maximum)
|
|
276
|
+
callback(new Error('HTML response exceeds the platform limit'));
|
|
277
|
+
else
|
|
278
|
+
callback(null, chunk);
|
|
279
|
+
} });
|
|
280
|
+
const marker = Duplex.fromWeb(createReleaseFormTransform({ deploymentId: this.options.target.deploymentId, documentUrl: new URL(this.options.request.url ?? '/', publicOrigin).href }));
|
|
281
|
+
this.transforms.push(limit, marker);
|
|
282
|
+
for (const stream of this.transforms)
|
|
283
|
+
stream.once('error', () => this.fail(502));
|
|
284
|
+
return source.pipe(limit).pipe(marker);
|
|
285
|
+
}
|
|
286
|
+
receiveChunk = (chunk) => {
|
|
287
|
+
if (this.stopped)
|
|
288
|
+
return;
|
|
289
|
+
this.receivedBytes += chunk.byteLength;
|
|
290
|
+
if (this.receivedBytes > this.maximum) {
|
|
291
|
+
this.fail(502);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (!this.options.response.write(chunk))
|
|
295
|
+
this.responseSource?.pause();
|
|
296
|
+
};
|
|
297
|
+
fail(status) {
|
|
298
|
+
if (this.stopped)
|
|
299
|
+
return;
|
|
300
|
+
this.upload?.pause();
|
|
301
|
+
this.incoming?.pause();
|
|
302
|
+
this.responseSource?.pause();
|
|
303
|
+
const response = this.options.response;
|
|
304
|
+
if (response.headersSent)
|
|
305
|
+
response.destroy();
|
|
306
|
+
else {
|
|
307
|
+
// Do not send application cookies, encodings or redirects with a gateway error.
|
|
308
|
+
for (const name of response.getHeaderNames()) {
|
|
309
|
+
if (name !== 'content-security-policy')
|
|
310
|
+
response.removeHeader(name);
|
|
311
|
+
}
|
|
312
|
+
response.statusCode = status;
|
|
313
|
+
enforceBrowserCachePolicy({ request: this.options.request, response });
|
|
314
|
+
response.writeHead(status, { 'content-type': 'application/json', connection: 'close' });
|
|
315
|
+
response.end(JSON.stringify({ error: status === 413 ? 'Request body exceeds the platform limit' : status === 504 ? 'Application request timed out' : status === 400 ? 'Invalid application request' : 'Application temporarily unavailable' }));
|
|
316
|
+
}
|
|
317
|
+
this.finish();
|
|
318
|
+
}
|
|
319
|
+
finish = () => {
|
|
320
|
+
if (this.stopped)
|
|
321
|
+
return;
|
|
322
|
+
this.stopped = true;
|
|
323
|
+
clearTimeout(this.timer);
|
|
324
|
+
this.controller.abort();
|
|
325
|
+
this.upload?.off('data', this.sendChunk);
|
|
326
|
+
this.incoming?.off('data', this.receiveChunk);
|
|
327
|
+
this.responseSource?.off('data', this.receiveChunk);
|
|
328
|
+
for (const transform of this.transforms)
|
|
329
|
+
transform.destroy();
|
|
330
|
+
if (!this.incoming?.complete || !this.outgoing?.writableFinished) {
|
|
331
|
+
this.outgoing?.destroy();
|
|
332
|
+
this.incoming?.destroy();
|
|
333
|
+
}
|
|
334
|
+
const { request, response } = this.options;
|
|
335
|
+
const closeUpload = () => {
|
|
336
|
+
if (!request.complete)
|
|
337
|
+
request.destroy();
|
|
338
|
+
if (this.upload !== request)
|
|
339
|
+
this.upload?.destroy();
|
|
340
|
+
};
|
|
341
|
+
if (response.writableFinished || response.destroyed)
|
|
342
|
+
closeUpload();
|
|
343
|
+
else
|
|
344
|
+
response.once('finish', closeUpload);
|
|
345
|
+
this.resolve?.();
|
|
346
|
+
};
|
|
347
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
type RejectionReason = 'inactive-release' | 'missing-release' | 'conflicting-release' | 'stale-release' | 'invalid-form' | 'prefix-limit';
|
|
3
|
+
export declare class ReleaseAdmissionError extends Error {
|
|
4
|
+
readonly reason: RejectionReason;
|
|
5
|
+
readonly status = 409;
|
|
6
|
+
readonly code = "update-required";
|
|
7
|
+
constructor(reason?: RejectionReason);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Admit a mutation against the release that rendered its document. No request is
|
|
11
|
+
* forwarded until admission succeeds. An admitted form exposes one continuation
|
|
12
|
+
* over the original iterator, including every byte inspected here.
|
|
13
|
+
*/
|
|
14
|
+
export declare function admitHostedMutation({ request, expectedDeploymentId, expectedFrameworkBuildId, active, published }: {
|
|
15
|
+
request: IncomingMessage;
|
|
16
|
+
expectedDeploymentId: string;
|
|
17
|
+
expectedFrameworkBuildId: string;
|
|
18
|
+
active: boolean;
|
|
19
|
+
published: boolean;
|
|
20
|
+
}): Promise<{
|
|
21
|
+
body?: AsyncIterable<Uint8Array>;
|
|
22
|
+
}>;
|
|
23
|
+
export {};
|