@sequenceholdings/artifact-studio 0.2.6 → 0.3.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.
- package/VERSION-PIN.md +12 -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 +134 -13
- package/dist/dev-link.d.ts +1 -1
- package/dist/dev-link.js +2 -1
- package/dist/gateway/admission.d.ts +56 -0
- package/dist/gateway/admission.js +134 -0
- package/dist/gateway/authorization-cache.d.ts +52 -0
- package/dist/gateway/authorization-cache.js +111 -0
- package/dist/gateway/control-admission.d.ts +48 -0
- package/dist/gateway/control-admission.js +127 -0
- package/dist/gateway/control.d.ts +94 -0
- package/dist/gateway/control.js +116 -0
- package/dist/gateway/hosted-proxy.d.ts +36 -0
- package/dist/gateway/hosted-proxy.js +344 -0
- package/dist/gateway/release-admission.d.ts +23 -0
- package/dist/gateway/release-admission.js +259 -0
- package/dist/gateway/server.d.ts +38 -0
- package/dist/gateway/server.js +382 -0
- package/dist/gateway/session.d.ts +72 -0
- package/dist/gateway/session.js +144 -0
- package/dist/gateway/usage.d.ts +70 -0
- package/dist/gateway/usage.js +116 -0
- package/dist/gateway-main.d.ts +1 -0
- package/dist/gateway-main.js +114 -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 +132 -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 +236 -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 +105 -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 +81 -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 +75 -0
- package/dist/server/request-context.js +107 -0
- package/dist/server/runtime-config.d.ts +54 -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 +157 -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/gitignore +9 -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 +22 -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/trusted-install.js +62 -2
- package/dist/tunnel/broker.d.ts +75 -0
- package/dist/tunnel/broker.js +463 -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 +78 -0
- package/dist/tunnel/protocol.js +210 -0
- package/dist/tunnel/redis-auth.d.ts +37 -0
- package/dist/tunnel/redis-auth.js +92 -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 +75 -0
- package/dist/tunnel/redis-types.js +9 -0
- package/dist/tunnel/redis.d.ts +56 -0
- package/dist/tunnel/redis.js +404 -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 +157 -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/gitignore +9 -0
- package/templates/nextjs/next.config.mjs +27 -0
- package/templates/nextjs/package.json +32 -0
- package/templates/nextjs/pnpm-workspace.yaml +22 -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,259 @@
|
|
|
1
|
+
import { artifactNextFrameworkBuildIdSchema } from '../runtime-contracts.js';
|
|
2
|
+
const RELEASE_FIELD = '__sequence_release';
|
|
3
|
+
const RELEASE_HEADER = 'x-sequence-artifact-release';
|
|
4
|
+
const FRAMEWORK_BUILD_HEADER = 'x-deployment-id';
|
|
5
|
+
const MAX_PREFIX_BYTES = 64 * 1024;
|
|
6
|
+
const MAX_RELEASE_BYTES = 255;
|
|
7
|
+
const RELEASE_ID = /^[a-zA-Z0-9_-]{1,255}$/;
|
|
8
|
+
const READ_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
9
|
+
export class ReleaseAdmissionError extends Error {
|
|
10
|
+
reason;
|
|
11
|
+
status = 409;
|
|
12
|
+
code = 'update-required';
|
|
13
|
+
constructor(reason = 'missing-release') {
|
|
14
|
+
super('Reload the application before submitting this request. The request was not executed.');
|
|
15
|
+
this.reason = reason;
|
|
16
|
+
this.name = 'ReleaseAdmissionError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function parseUrlEncoded({ prefix, ended }) {
|
|
20
|
+
const separator = prefix.indexOf(0x26);
|
|
21
|
+
if (separator < 0 && !ended)
|
|
22
|
+
return undefined;
|
|
23
|
+
const first = prefix.subarray(0, separator < 0 ? prefix.length : separator).toString('utf8');
|
|
24
|
+
const equals = first.indexOf('=');
|
|
25
|
+
if (equals < 0)
|
|
26
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
27
|
+
try {
|
|
28
|
+
const name = decodeURIComponent(first.slice(0, equals).replace(/\+/g, ' '));
|
|
29
|
+
const release = decodeURIComponent(first.slice(equals + 1).replace(/\+/g, ' '));
|
|
30
|
+
if (name !== RELEASE_FIELD || Buffer.byteLength(release) > MAX_RELEASE_BYTES || !RELEASE_ID.test(release)) {
|
|
31
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
32
|
+
}
|
|
33
|
+
return release;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function multipartParser({ boundary }) {
|
|
40
|
+
const opening = Buffer.from(`--${boundary}\r\n`);
|
|
41
|
+
const delimiter = Buffer.from(`\r\n--${boundary}`);
|
|
42
|
+
return ({ prefix, ended }) => {
|
|
43
|
+
if (!prefix.subarray(0, Math.min(prefix.length, opening.length)).equals(opening.subarray(0, Math.min(prefix.length, opening.length)))) {
|
|
44
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
45
|
+
}
|
|
46
|
+
if (prefix.length < opening.length) {
|
|
47
|
+
if (ended)
|
|
48
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const headerEnd = prefix.indexOf('\r\n\r\n', opening.length);
|
|
52
|
+
if (headerEnd < 0) {
|
|
53
|
+
if (ended)
|
|
54
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const headers = new Map();
|
|
58
|
+
for (const line of prefix.subarray(opening.length, headerEnd).toString('latin1').split('\r\n')) {
|
|
59
|
+
const colon = line.indexOf(':');
|
|
60
|
+
if (colon <= 0)
|
|
61
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
62
|
+
const name = line.slice(0, colon).toLowerCase();
|
|
63
|
+
const value = line.slice(colon + 1).trim();
|
|
64
|
+
if (!['content-disposition', 'content-type'].includes(name) || headers.has(name)) {
|
|
65
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
66
|
+
}
|
|
67
|
+
headers.set(name, value);
|
|
68
|
+
}
|
|
69
|
+
const disposition = headers.get('content-disposition')?.split(';').map((part) => part.trim());
|
|
70
|
+
if (!disposition || disposition.length !== 2 || disposition[0].toLowerCase() !== 'form-data') {
|
|
71
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
72
|
+
}
|
|
73
|
+
const field = disposition[1];
|
|
74
|
+
const equals = field.indexOf('=');
|
|
75
|
+
if (equals < 0 || field.slice(0, equals).toLowerCase() !== 'name' ||
|
|
76
|
+
![RELEASE_FIELD, `"${RELEASE_FIELD}"`].includes(field.slice(equals + 1))) {
|
|
77
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
78
|
+
}
|
|
79
|
+
const contentType = headers.get('content-type');
|
|
80
|
+
if (contentType && !/^text\/plain(?:\s*;\s*charset=(?:utf-8|"utf-8"))?$/i.test(contentType)) {
|
|
81
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
82
|
+
}
|
|
83
|
+
const valueStart = headerEnd + 4;
|
|
84
|
+
const valueEnd = prefix.indexOf(delimiter, valueStart);
|
|
85
|
+
if (valueEnd < 0) {
|
|
86
|
+
if (ended || prefix.length - valueStart > MAX_RELEASE_BYTES + delimiter.length - 1) {
|
|
87
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
if (valueEnd - valueStart > MAX_RELEASE_BYTES)
|
|
92
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
93
|
+
const suffixOffset = valueEnd + delimiter.length;
|
|
94
|
+
if (prefix.length < suffixOffset + 2) {
|
|
95
|
+
if (ended)
|
|
96
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const suffix = prefix.subarray(suffixOffset, suffixOffset + 2).toString('ascii');
|
|
100
|
+
if (suffix !== '\r\n' && suffix !== '--')
|
|
101
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
102
|
+
if (suffix === '--') {
|
|
103
|
+
const closingEnd = suffixOffset + 2;
|
|
104
|
+
if (prefix.length < closingEnd + 2) {
|
|
105
|
+
if (!ended)
|
|
106
|
+
return undefined;
|
|
107
|
+
if (prefix.length !== closingEnd)
|
|
108
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
109
|
+
}
|
|
110
|
+
else if (!prefix.subarray(closingEnd, closingEnd + 2).equals(Buffer.from('\r\n'))) {
|
|
111
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const release = prefix.subarray(valueStart, valueEnd).toString('utf8');
|
|
115
|
+
if (!RELEASE_ID.test(release))
|
|
116
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
117
|
+
return release;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function getFormParser({ contentType }) {
|
|
121
|
+
if (typeof contentType !== 'string')
|
|
122
|
+
throw new ReleaseAdmissionError('missing-release');
|
|
123
|
+
const [mediaType, ...parameters] = contentType.split(';').map((part) => part.trim());
|
|
124
|
+
if (mediaType.toLowerCase() === 'application/x-www-form-urlencoded')
|
|
125
|
+
return parseUrlEncoded;
|
|
126
|
+
if (mediaType.toLowerCase() !== 'multipart/form-data')
|
|
127
|
+
throw new ReleaseAdmissionError('missing-release');
|
|
128
|
+
const boundaries = parameters.filter((parameter) => parameter.slice(0, parameter.indexOf('=')).toLowerCase() === 'boundary');
|
|
129
|
+
if (boundaries.length !== 1)
|
|
130
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
131
|
+
const raw = boundaries[0].slice(boundaries[0].indexOf('=') + 1);
|
|
132
|
+
const boundary = raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1) : raw;
|
|
133
|
+
if (!/^[0-9A-Za-z'()+_,./:=? -]{1,70}$/.test(boundary) || boundary.endsWith(' ') ||
|
|
134
|
+
(!raw.startsWith('"') && boundary.includes(' '))) {
|
|
135
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
136
|
+
}
|
|
137
|
+
return multipartParser({ boundary });
|
|
138
|
+
}
|
|
139
|
+
function checkRelease({ release, expectedDeploymentId }) {
|
|
140
|
+
if (!RELEASE_ID.test(release) || release !== expectedDeploymentId)
|
|
141
|
+
throw new ReleaseAdmissionError('stale-release');
|
|
142
|
+
}
|
|
143
|
+
function readReleaseHeader({ request, name }) {
|
|
144
|
+
const value = request.headers[name];
|
|
145
|
+
// Inspect raw headers too: Node may discard rather than join duplicate values.
|
|
146
|
+
const count = request.rawHeaders.filter((_, index) => index % 2 === 0 && request.rawHeaders[index].toLowerCase() === name).length;
|
|
147
|
+
if (count > 1)
|
|
148
|
+
throw new ReleaseAdmissionError('conflicting-release');
|
|
149
|
+
if (value === undefined)
|
|
150
|
+
return undefined;
|
|
151
|
+
if (typeof value !== 'string' || !RELEASE_ID.test(value))
|
|
152
|
+
throw new ReleaseAdmissionError('conflicting-release');
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
/** Framework hints identify compiled assets, never the Atlas activation authorizing a mutation. */
|
|
156
|
+
function validateFrameworkHints({ request, expectedFrameworkBuildId }) {
|
|
157
|
+
const header = readReleaseHeader({ request, name: FRAMEWORK_BUILD_HEADER });
|
|
158
|
+
let query;
|
|
159
|
+
try {
|
|
160
|
+
query = new URL(request.url ?? '/', 'http://release.invalid').searchParams.getAll('dpl');
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
throw new ReleaseAdmissionError('conflicting-release');
|
|
164
|
+
}
|
|
165
|
+
if (!artifactNextFrameworkBuildIdSchema.safeParse(expectedFrameworkBuildId).success ||
|
|
166
|
+
(header !== undefined && !artifactNextFrameworkBuildIdSchema.safeParse(header).success) ||
|
|
167
|
+
query.length > 1 || (query.length === 1 && !artifactNextFrameworkBuildIdSchema.safeParse(query[0]).success)) {
|
|
168
|
+
throw new ReleaseAdmissionError('conflicting-release');
|
|
169
|
+
}
|
|
170
|
+
if (header !== undefined && query.length === 1 && header !== query[0]) {
|
|
171
|
+
throw new ReleaseAdmissionError('conflicting-release');
|
|
172
|
+
}
|
|
173
|
+
const frameworkBuildId = header ?? query[0];
|
|
174
|
+
if (frameworkBuildId !== undefined && frameworkBuildId !== expectedFrameworkBuildId) {
|
|
175
|
+
throw new ReleaseAdmissionError('stale-release');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function bytes({ value }) {
|
|
179
|
+
if (!(value instanceof Uint8Array))
|
|
180
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Admit a mutation against the release that rendered its document. No request is
|
|
185
|
+
* forwarded until admission succeeds. An admitted form exposes one continuation
|
|
186
|
+
* over the original iterator, including every byte inspected here.
|
|
187
|
+
*/
|
|
188
|
+
export async function admitHostedMutation({ request, expectedDeploymentId, expectedFrameworkBuildId, active, published }) {
|
|
189
|
+
const method = (request.method ?? 'GET').toUpperCase();
|
|
190
|
+
validateFrameworkHints({ request, expectedFrameworkBuildId });
|
|
191
|
+
if (READ_METHODS.has(method))
|
|
192
|
+
return {};
|
|
193
|
+
if (published && !active)
|
|
194
|
+
throw new ReleaseAdmissionError('inactive-release');
|
|
195
|
+
const release = readReleaseHeader({ request, name: RELEASE_HEADER });
|
|
196
|
+
if (release !== undefined) {
|
|
197
|
+
checkRelease({ release, expectedDeploymentId });
|
|
198
|
+
return {};
|
|
199
|
+
}
|
|
200
|
+
if (method !== 'POST')
|
|
201
|
+
throw new ReleaseAdmissionError('missing-release');
|
|
202
|
+
const parse = getFormParser({ contentType: request.headers['content-type'] });
|
|
203
|
+
// A normal for-await break destroys IncomingMessage. Retain this one iterator
|
|
204
|
+
// while peeking, then hand its continuation to the proxy without a second read.
|
|
205
|
+
const iterator = request.iterator({ destroyOnReturn: false });
|
|
206
|
+
const buffered = [];
|
|
207
|
+
const prefixBuffer = Buffer.alloc(MAX_PREFIX_BYTES);
|
|
208
|
+
let prefixLength = 0;
|
|
209
|
+
let ended = false;
|
|
210
|
+
try {
|
|
211
|
+
while (true) {
|
|
212
|
+
const release = parse({ prefix: prefixBuffer.subarray(0, prefixLength), ended });
|
|
213
|
+
if (release !== undefined) {
|
|
214
|
+
checkRelease({ release, expectedDeploymentId });
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
if (ended)
|
|
218
|
+
throw new ReleaseAdmissionError('invalid-form');
|
|
219
|
+
if (prefixLength >= MAX_PREFIX_BYTES)
|
|
220
|
+
throw new ReleaseAdmissionError('prefix-limit');
|
|
221
|
+
const next = await iterator.next();
|
|
222
|
+
ended = next.done === true;
|
|
223
|
+
if (!ended) {
|
|
224
|
+
const chunk = bytes({ value: next.value });
|
|
225
|
+
buffered.push(chunk);
|
|
226
|
+
// Inspect at most 64 KiB even when a transport read returns a larger
|
|
227
|
+
// chunk. Keep that original chunk for lossless, zero-copy forwarding.
|
|
228
|
+
const inspected = chunk.subarray(0, MAX_PREFIX_BYTES - prefixLength);
|
|
229
|
+
prefixBuffer.set(inspected, prefixLength);
|
|
230
|
+
prefixLength += inspected.length;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
await iterator.return?.();
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
async function* continuation() {
|
|
239
|
+
try {
|
|
240
|
+
for (const chunk of buffered)
|
|
241
|
+
yield chunk;
|
|
242
|
+
if (ended)
|
|
243
|
+
return;
|
|
244
|
+
while (true) {
|
|
245
|
+
const next = await iterator.next();
|
|
246
|
+
if (next.done)
|
|
247
|
+
return;
|
|
248
|
+
yield bytes({ value: next.value });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
// The transport owns abort/connection-close behavior. Returning this
|
|
253
|
+
// non-destructive iterator releases its listeners without losing a 409
|
|
254
|
+
// or upstream-error response by prematurely destroying the client socket.
|
|
255
|
+
await iterator.return?.();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { body: continuation() };
|
|
259
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
|
+
import { type TunnelLease, type TunnelBrokerOptions } from '../tunnel/index.js';
|
|
3
|
+
import { ArtifactGatewayControl } from './control.js';
|
|
4
|
+
import { type ArtifactRequestAdmission } from './admission.js';
|
|
5
|
+
import { type GatewayControlAdmission } from './control-admission.js';
|
|
6
|
+
export interface ArtifactGatewayOptions {
|
|
7
|
+
control: ArtifactGatewayControl;
|
|
8
|
+
runtimeDomain: string;
|
|
9
|
+
parentOrigin: string;
|
|
10
|
+
gatewayOrigin: string;
|
|
11
|
+
sessionKey: string;
|
|
12
|
+
contextPrivateKey: string;
|
|
13
|
+
contextKeyId: string;
|
|
14
|
+
/** Unique per process, never a shared Cloud Run revision name. */
|
|
15
|
+
nodeId: string;
|
|
16
|
+
routing: TunnelBrokerOptions['routing'];
|
|
17
|
+
relay?: TunnelBrokerOptions['relay'];
|
|
18
|
+
resolveLease?: TunnelBrokerOptions['resolveLease'];
|
|
19
|
+
renewLease?: (lease: TunnelLease) => Promise<boolean>;
|
|
20
|
+
ready?: () => boolean;
|
|
21
|
+
admission?: Pick<ArtifactRequestAdmission, 'acquire'>;
|
|
22
|
+
/** Rate limits `/_sequence/session` and `/_sequence/tunnel` before they reach Atlas. Without it
|
|
23
|
+
* both endpoints fail closed. */
|
|
24
|
+
controlAdmission?: Pick<GatewayControlAdmission, 'admit'>;
|
|
25
|
+
/** Reuse of a positive Atlas authorization (default GATEWAY_AUTHORIZATION_TTL_MS; 0 disables). */
|
|
26
|
+
authorizationCacheTtlMs?: number;
|
|
27
|
+
onError?: (event: {
|
|
28
|
+
kind: string;
|
|
29
|
+
requestId?: string;
|
|
30
|
+
count?: number;
|
|
31
|
+
}) => void;
|
|
32
|
+
}
|
|
33
|
+
export declare function createArtifactGateway(options: ArtifactGatewayOptions): {
|
|
34
|
+
server: import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
35
|
+
/** Observations recorded but not yet confirmed by Atlas: what a forced exit would lose. */
|
|
36
|
+
readonly pendingUsage: number;
|
|
37
|
+
drain(): Promise<void>;
|
|
38
|
+
};
|