@sequenceholdings/artifact-studio 0.6.3 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -6
- package/VERSION-PIN.md +56 -1
- package/dist/gateway/admission.d.ts +23 -1
- package/dist/gateway/admission.js +30 -13
- package/dist/gateway/control.d.ts +40 -0
- package/dist/gateway/control.js +76 -7
- package/dist/gateway/hosted-proxy.d.ts +11 -8
- package/dist/gateway/hosted-proxy.js +99 -91
- package/dist/gateway/ingress-admission.d.ts +77 -0
- package/dist/gateway/ingress-admission.js +150 -0
- package/dist/gateway/ingress-audit.d.ts +46 -0
- package/dist/gateway/ingress-audit.js +84 -0
- package/dist/gateway/ingress-context.d.ts +57 -0
- package/dist/gateway/ingress-context.js +57 -0
- package/dist/gateway/ingress-development.d.ts +12 -0
- package/dist/gateway/ingress-development.js +23 -0
- package/dist/gateway/ingress-handler.d.ts +44 -0
- package/dist/gateway/ingress-handler.js +320 -0
- package/dist/gateway/ingress-pass.d.ts +83 -0
- package/dist/gateway/ingress-pass.js +143 -0
- package/dist/gateway/ingress-response.d.ts +74 -0
- package/dist/gateway/ingress-response.js +98 -0
- package/dist/gateway/ingress-runtime.d.ts +86 -0
- package/dist/gateway/ingress-runtime.js +126 -0
- package/dist/gateway/ingress-snapshot.d.ts +54 -0
- package/dist/gateway/ingress-snapshot.js +63 -0
- package/dist/gateway/ingress-stages.d.ts +60 -0
- package/dist/gateway/ingress-stages.js +387 -0
- package/dist/gateway/ingress.d.ts +259 -0
- package/dist/gateway/ingress.js +204 -0
- package/dist/gateway/lattice-invoke.js +2 -0
- package/dist/gateway/lattice-upstream.d.ts +3 -4
- package/dist/gateway/lattice-upstream.js +11 -9
- package/dist/gateway/runtime-transport.d.ts +17 -0
- package/dist/gateway/runtime-transport.js +26 -0
- package/dist/gateway/server.d.ts +3 -0
- package/dist/gateway/server.js +21 -0
- package/dist/gateway/session-response.d.ts +15 -0
- package/dist/gateway/session-response.js +40 -0
- package/dist/gateway/usage.d.ts +8 -1
- package/dist/gateway-main.js +32 -8
- package/dist/manifest-next-ingress.d.ts +5 -2
- package/dist/manifest-next-ingress.js +27 -2
- package/dist/manifest-next.d.ts +20 -6
- package/dist/manifest-next.js +13 -5
- package/dist/manifest.d.ts +7 -2
- package/dist/next-project.d.ts +4 -0
- package/dist/sdk.d.ts +12 -3
- package/dist/server/broker-contract.d.ts +62 -0
- package/dist/server/broker-contract.js +43 -1
- package/dist/server/client.js +38 -13
- package/dist/server/external-context.d.ts +8 -1
- package/dist/server/external-context.js +7 -2
- package/dist/server/internal/v1-context.js +17 -1
- package/dist/server/internal/verify-any.d.ts +11 -2
- package/dist/server/internal/verify-any.js +11 -3
- package/dist/server/process-run-sharing.d.ts +86 -0
- package/dist/server/process-run-sharing.js +29 -0
- package/dist/server/process-runs-page.d.ts +39 -0
- package/dist/server/process-runs-page.js +10 -0
- package/dist/server/processes.d.ts +8 -9
- package/dist/server/processes.js +4 -0
- package/dist/server/request-context.js +10 -3
- package/dist/server/runtime-config.d.ts +10 -1
- package/dist/server/runtime-config.js +11 -1
- package/dist/server/testing.d.ts +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/tunnel/broker.d.ts +18 -0
- package/dist/tunnel/broker.js +175 -17
- package/dist/tunnel/client.d.ts +13 -0
- package/dist/tunnel/client.js +40 -2
- package/dist/tunnel/external.d.ts +51 -0
- package/dist/tunnel/external.js +10 -0
- package/dist/tunnel/index.d.ts +1 -1
- package/dist/tunnel/protocol.d.ts +20 -2
- package/dist/tunnel/protocol.js +42 -6
- package/package.json +4 -4
|
@@ -22,6 +22,16 @@ export const artifactRuntimeConfigSchema = z.object({
|
|
|
22
22
|
devSessionId: z.string().min(1).max(255).optional(),
|
|
23
23
|
deploymentId: z.string().min(1).max(255).optional(),
|
|
24
24
|
}).strict().refine(value => !(value.devSessionId && value.deploymentId), 'Runtime cannot be both local and hosted');
|
|
25
|
+
/**
|
|
26
|
+
* Marks a lease file's configuration. Non-enumerable, so the strict schema still accepts the
|
|
27
|
+
* object if a caller hands it back as an explicit `runtime` (which then drops the marker).
|
|
28
|
+
*/
|
|
29
|
+
function developmentLease(config) {
|
|
30
|
+
// A lease belongs to exactly one development session; anything else is not a development lease.
|
|
31
|
+
if (!config.devSessionId || config.deploymentId)
|
|
32
|
+
return config;
|
|
33
|
+
return Object.defineProperty(config, 'development', { value: true, enumerable: false, writable: false, configurable: false });
|
|
34
|
+
}
|
|
25
35
|
/** Credentials are platform-provided and rotated by the CLI, never inferred from ambient cloud auth. */
|
|
26
36
|
export async function readArtifactRuntimeConfig({ file = process.env.SEQUENCE_ARTIFACT_RUNTIME_FILE } = {}) {
|
|
27
37
|
if (!file) {
|
|
@@ -42,7 +52,7 @@ export async function readArtifactRuntimeConfig({ file = process.env.SEQUENCE_AR
|
|
|
42
52
|
const stat = await handle.stat();
|
|
43
53
|
if (!stat.isFile() || stat.size > 65536 || (process.platform !== 'win32' && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())))
|
|
44
54
|
throw new Error('Artifact runtime credential file must be owner-readable only, regular, and no larger than 64 KiB');
|
|
45
|
-
return artifactRuntimeConfigSchema.parse(JSON.parse(await handle.readFile('utf8')));
|
|
55
|
+
return developmentLease(artifactRuntimeConfigSchema.parse(JSON.parse(await handle.readFile('utf8'))));
|
|
46
56
|
}
|
|
47
57
|
finally {
|
|
48
58
|
await handle.close();
|
package/dist/server/testing.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface SignTestExternalContextInput {
|
|
|
19
19
|
/** Overrides on a verified-borrower default principal. */
|
|
20
20
|
principal?: Partial<Omit<ExternalPrincipalClaims, 'kind'>>;
|
|
21
21
|
serviceAccountId: string;
|
|
22
|
-
/** Defaults to `published`. Verifiers
|
|
22
|
+
/** Defaults to `published`. Verifiers accept `development` only under a development lease, for its session. */
|
|
23
23
|
mode?: RequestContextV2External['mode'];
|
|
24
24
|
deploymentId?: string | null;
|
|
25
25
|
devSessionId?: string | null;
|
package/dist/server.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import 'server-only';
|
|
|
2
2
|
export { createArtifactServerClient } from './server/client.js';
|
|
3
3
|
export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './server/errors.js';
|
|
4
4
|
export type { ArtifactAtlasRefusal } from './server/errors.js';
|
|
5
|
-
export type { ArtifactAuditEvent, ArtifactOrmOperations, ArtifactOrmOptions, ArtifactProcessAdvance, ArtifactProcessAdvanceResult, ArtifactProcessArtifactRef, ArtifactProcessAssign, ArtifactProcessAssignResult, ArtifactProcessDefinition, ArtifactProcessDescribeQuery, ArtifactProcessEdge, ArtifactProcessHumanNode, ArtifactProcessNode, ArtifactProcessNodeBase, ArtifactProcessNodeState, ArtifactProcessPrincipal, ArtifactProcessPrincipalSearch, ArtifactProcessPrincipals, ArtifactProcessRun, ArtifactProcessRunRecord, ArtifactProcessRunSnapshot, ArtifactProcessRunsQuery, ArtifactProcessRunViewerChange, ArtifactProcessRunViewers, ArtifactProcessStart, ArtifactProcessStepRecord, ArtifactProcessTask, ArtifactProcessTasksQuery, ArtifactServerAudit, ArtifactServerClient, ArtifactServerClientOptions, ArtifactServerContext, ArtifactServerFetchOptions, ArtifactServerFlags, ArtifactServerProcesses, } from './server/client.js';
|
|
6
|
-
export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
|
|
5
|
+
export type { ArtifactAuditEvent, ArtifactOrmOperations, ArtifactOrmOptions, ArtifactProcessAdvance, ArtifactProcessAdvanceResult, ArtifactProcessArtifactRef, ArtifactProcessAssign, ArtifactProcessAssignResult, ArtifactProcessDefinition, ArtifactProcessDescribeQuery, ArtifactProcessEdge, ArtifactProcessHumanNode, ArtifactProcessNode, ArtifactProcessNodeBase, ArtifactProcessNodeState, ArtifactProcessPrincipal, ArtifactProcessPrincipalSearch, ArtifactProcessPrincipals, ArtifactProcessRun, ArtifactProcessRunRecord, ArtifactProcessRunSnapshot, ArtifactProcessRunSummary, ArtifactProcessRunsPage, ArtifactProcessRunsPageQuery, ArtifactProcessRunsQuery, ArtifactProcessRunViewerChange, ArtifactProcessRunViewers, ArtifactProcessStart, ArtifactProcessStepRecord, ArtifactProcessShareRun, ArtifactProcessShareRunResult, ArtifactProcessRunSharingAuthoritiesQuery, ArtifactProcessRunSharingAuthoritiesPage, ArtifactProcessRunSharingAuthority, ArtifactProcessRunSharingDelegate, ArtifactProcessRevokeRunSharingAuthority, ArtifactProcessRevokeRunSharingAuthorityResult, ArtifactProcessTask, ArtifactProcessTasksQuery, ArtifactServerAudit, ArtifactServerClient, ArtifactServerClientOptions, ArtifactServerContext, ArtifactServerFetchOptions, ArtifactServerFlags, ArtifactServerProcesses, } from './server/client.js';
|
|
6
|
+
export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_RUN_ENTITIES_LIMIT, ARTIFACT_PROCESS_SHARE_NODES_LIMIT, ARTIFACT_PROCESS_SHARING_AUTHORITIES_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
|
|
7
7
|
export type { ArtifactAuditAction, ArtifactProcessPrincipalType, ArtifactProcessRunStatus, ArtifactProcessTaskView, } from './server/broker-contract.js';
|
|
8
8
|
export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
|
|
9
9
|
export type { ArtifactRuntimeConfig, ArtifactRequestHeaders } from './server/runtime-config.js';
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
2
|
export { createArtifactServerClient } from './server/client.js';
|
|
3
3
|
export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './server/errors.js';
|
|
4
|
-
export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
|
|
4
|
+
export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_RUN_ENTITIES_LIMIT, ARTIFACT_PROCESS_SHARE_NODES_LIMIT, ARTIFACT_PROCESS_SHARING_AUTHORITIES_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
|
|
5
5
|
export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
|
|
6
6
|
export { ArtifactContextError, getArtifactBrowserBootstrap } from './server/request-context.js';
|
|
7
7
|
export { verifyArtifactExternalContext } from './server/external-context.js';
|
package/dist/tunnel/broker.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
2
2
|
import type { Duplex } from 'node:stream';
|
|
3
3
|
import { type TunnelLimits } from './peer.js';
|
|
4
4
|
import type { TunnelRelay } from './transport.js';
|
|
5
|
+
import { type TunnelExternalRequest } from './external.js';
|
|
6
|
+
export type { TunnelExternalFailure, TunnelExternalRequest } from './external.js';
|
|
5
7
|
export interface TunnelLease {
|
|
6
8
|
sessionId: string;
|
|
7
9
|
epoch: number;
|
|
@@ -53,12 +55,28 @@ export declare class ArtifactTunnelBroker {
|
|
|
53
55
|
constructor(options: TunnelBrokerOptions);
|
|
54
56
|
handleTunnelUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): Promise<void>;
|
|
55
57
|
handleHttp(request: IncomingMessage, response: ServerResponse, sessionId: string): Promise<void>;
|
|
58
|
+
/** Proxies an admitted external ingress request over the session's tunnel. Never throws. */
|
|
59
|
+
handleExternalHttp(input: TunnelExternalRequest): Promise<void>;
|
|
56
60
|
handleWebSocketUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer, sessionId: string): Promise<void>;
|
|
57
61
|
private authorize;
|
|
62
|
+
/**
|
|
63
|
+
* The live tunnel for an external request: the session's current lease, for the granted project
|
|
64
|
+
* and environment, still valid. Null otherwise. No browser owner is compared: Atlas's grant
|
|
65
|
+
* already checked the session and its owner's grant on the ingress service account.
|
|
66
|
+
*/
|
|
67
|
+
private externalConnection;
|
|
58
68
|
private valid;
|
|
59
69
|
private remoteConnection;
|
|
60
70
|
private attach;
|
|
61
71
|
private receive;
|
|
72
|
+
private receiveHead;
|
|
73
|
+
private receiveBody;
|
|
74
|
+
/**
|
|
75
|
+
* Backpressure from an external caller abandons the request after the idle timeout instead of
|
|
76
|
+
* holding its stream (and the caller's admission permit) until the request timeout. A write the
|
|
77
|
+
* abort interrupted is not a tunnel failure, so the session's other streams carry on.
|
|
78
|
+
*/
|
|
79
|
+
private writeExternalChunk;
|
|
62
80
|
private finish;
|
|
63
81
|
private abort;
|
|
64
82
|
/** Immediate revocation and graceful service shutdown hook. */
|
package/dist/tunnel/broker.js
CHANGED
|
@@ -2,6 +2,7 @@ import { WebSocketServer } from 'ws';
|
|
|
2
2
|
import { TunnelPeer, tunnelLimits } from './peer.js';
|
|
3
3
|
import { assertSafeRequest, encodeTunnelFrame, isSharedCacheDirective, normalizePublicOrigin, sanitizeRequestHeaders, sanitizeResponseHeaders, SHARED_CACHE_REFUSAL, TUNNEL_PROTOCOL } from './protocol.js';
|
|
4
4
|
import { forwardWebSocketMessages, MessageBudget, permittedCloseCode, sendWebSocket, WebSocketMessage, writeChunk } from './io.js';
|
|
5
|
+
import { declaredLength, isExternalFailure } from './external.js';
|
|
5
6
|
const UPGRADE_REASONS = { 403: 'Forbidden', 429: 'Too Many Requests', 503: 'Service Unavailable' };
|
|
6
7
|
function rejectUpgrade(socket, status) {
|
|
7
8
|
socket.end(`HTTP/1.1 ${status} ${UPGRADE_REASONS[status] ?? 'Service Unavailable'}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
|
|
@@ -194,6 +195,91 @@ export class ArtifactTunnelBroker {
|
|
|
194
195
|
rejectHttp(response, 403);
|
|
195
196
|
}
|
|
196
197
|
}
|
|
198
|
+
/** Proxies an admitted external ingress request over the session's tunnel. Never throws. */
|
|
199
|
+
async handleExternalHttp(input) {
|
|
200
|
+
const { request, response, sessionId } = input;
|
|
201
|
+
const method = request.method ?? 'GET';
|
|
202
|
+
const maxRequestBytes = Math.min(input.maxRequestBytes, this.limits.maxBodyBytes);
|
|
203
|
+
try {
|
|
204
|
+
assertSafeRequest(method, request.url ?? '/');
|
|
205
|
+
if (!input.requestContext || /[\r\n]/.test(input.requestContext))
|
|
206
|
+
throw new Error('Invalid request context');
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
input.fail(400);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const length = request.headers['content-length'];
|
|
213
|
+
if (length !== undefined && (!/^(0|[1-9][0-9]*)$/.test(length) || Number(length) > maxRequestBytes)) {
|
|
214
|
+
input.fail(413);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
let connection;
|
|
218
|
+
try {
|
|
219
|
+
connection = await this.externalConnection(input);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
connection = null;
|
|
223
|
+
}
|
|
224
|
+
// No tunnel, or one for another project: the session ended or never connected.
|
|
225
|
+
if (!connection) {
|
|
226
|
+
input.fail(503);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
let headers;
|
|
230
|
+
try {
|
|
231
|
+
headers = sanitizeRequestHeaders(request.headers, input.publicOrigin, { mode: 'ingress', appHeaders: input.appHeaders });
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
input.fail(400);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
headers['x-sequence-artifact-context'] = input.requestContext;
|
|
238
|
+
let id;
|
|
239
|
+
try {
|
|
240
|
+
id = connection.peer.open();
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
input.fail(503);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const active = connection;
|
|
247
|
+
const streamId = id;
|
|
248
|
+
const timer = setTimeout(() => this.abort(active, streamId, 504), Math.min(input.timeoutMs, this.limits.requestTimeoutMs));
|
|
249
|
+
timer.unref();
|
|
250
|
+
connection.streams.set(id, {
|
|
251
|
+
request, response, timer, bytes: 0, receivedHeaders: false,
|
|
252
|
+
message: new WebSocketMessage(this.limits.maxWebSocketMessageBytes, connection.messageBudget),
|
|
253
|
+
external: {
|
|
254
|
+
writeHead: input.writeHead, fail: input.fail, idleWriteTimeoutMs: input.idleWriteTimeoutMs,
|
|
255
|
+
maxResponseBytes: Math.min(input.maxResponseBytes, this.limits.maxBodyBytes),
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
// Even a response the gateway finished itself (its admission timeout's 504) stops the stream;
|
|
259
|
+
// a stream that completed normally is already gone, so this is a no-op for it.
|
|
260
|
+
response.once('close', () => this.abort(active, streamId));
|
|
261
|
+
request.once('aborted', () => this.abort(active, streamId));
|
|
262
|
+
try {
|
|
263
|
+
await connection.peer.send({ type: 'request', id, method, path: request.url ?? '/', headers, websocket: false });
|
|
264
|
+
let total = 0;
|
|
265
|
+
for await (const chunk of input.body ?? request) {
|
|
266
|
+
if (!connection.streams.has(id))
|
|
267
|
+
return;
|
|
268
|
+
const bytes = Buffer.from(chunk);
|
|
269
|
+
total += bytes.length;
|
|
270
|
+
if (total > maxRequestBytes) {
|
|
271
|
+
this.abort(connection, id, 413);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
await connection.peer.data(id, bytes);
|
|
275
|
+
}
|
|
276
|
+
if (connection.streams.has(id))
|
|
277
|
+
await connection.peer.send({ type: 'end', id });
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
this.abort(connection, id, 503);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
197
283
|
async handleWebSocketUpgrade(request, socket, head, sessionId) {
|
|
198
284
|
socket.pause();
|
|
199
285
|
try {
|
|
@@ -255,6 +341,29 @@ export class ArtifactTunnelBroker {
|
|
|
255
341
|
return null;
|
|
256
342
|
return { connection, headers };
|
|
257
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* The live tunnel for an external request: the session's current lease, for the granted project
|
|
346
|
+
* and environment, still valid. Null otherwise. No browser owner is compared: Atlas's grant
|
|
347
|
+
* already checked the session and its owner's grant on the ingress service account.
|
|
348
|
+
*/
|
|
349
|
+
async externalConnection({ sessionId, projectId, environment }) {
|
|
350
|
+
if (this.closing)
|
|
351
|
+
return null;
|
|
352
|
+
const granted = (lease) => lease.sessionId === sessionId && lease.projectId === projectId && lease.environment === environment;
|
|
353
|
+
let connection = this.connections.get(sessionId);
|
|
354
|
+
if (this.options.relay && this.options.resolveLease) {
|
|
355
|
+
const lease = await this.options.resolveLease(sessionId);
|
|
356
|
+
if (!lease || !granted(lease) || !(await this.options.validateLease(lease)))
|
|
357
|
+
return null;
|
|
358
|
+
if (!connection || connection.lease.epoch !== lease.epoch)
|
|
359
|
+
connection = await this.remoteConnection(lease);
|
|
360
|
+
else
|
|
361
|
+
connection.lease.expiresAt = lease.expiresAt;
|
|
362
|
+
}
|
|
363
|
+
if (!connection || !granted(connection.lease) || !(await this.valid(connection)))
|
|
364
|
+
return null;
|
|
365
|
+
return connection;
|
|
366
|
+
}
|
|
258
367
|
async valid(connection) {
|
|
259
368
|
const owner = await this.options.routing.lookup(connection.lease.sessionId);
|
|
260
369
|
if (this.options.resolveLease) {
|
|
@@ -330,10 +439,7 @@ export class ArtifactTunnelBroker {
|
|
|
330
439
|
if (frame.type === 'request')
|
|
331
440
|
throw new Error('Developer cannot initiate gateway requests');
|
|
332
441
|
if (frame.type === 'response') {
|
|
333
|
-
|
|
334
|
-
throw new Error('Unexpected response');
|
|
335
|
-
stream.receivedHeaders = true;
|
|
336
|
-
stream.response.writeHead(frame.status, developmentResponseHeaders({ response: stream.response, headers: sanitizeResponseHeaders(frame.headers) }));
|
|
442
|
+
this.receiveHead(connection, stream, frame);
|
|
337
443
|
return;
|
|
338
444
|
}
|
|
339
445
|
if (frame.type === 'data') {
|
|
@@ -348,17 +454,8 @@ export class ArtifactTunnelBroker {
|
|
|
348
454
|
}
|
|
349
455
|
}
|
|
350
456
|
}
|
|
351
|
-
else
|
|
352
|
-
|
|
353
|
-
throw new Error('Unexpected response body');
|
|
354
|
-
const bytes = Buffer.from(frame.data, 'base64');
|
|
355
|
-
stream.bytes += bytes.length;
|
|
356
|
-
if (stream.bytes > this.limits.maxBodyBytes) {
|
|
357
|
-
this.abort(connection, frame.id, 502);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
await writeChunk(stream.response, bytes);
|
|
361
|
-
}
|
|
457
|
+
else
|
|
458
|
+
await this.receiveBody(connection, stream, frame);
|
|
362
459
|
return;
|
|
363
460
|
}
|
|
364
461
|
if (frame.type === 'end') {
|
|
@@ -399,6 +496,62 @@ export class ArtifactTunnelBroker {
|
|
|
399
496
|
upgrade.socket.resume();
|
|
400
497
|
}
|
|
401
498
|
}
|
|
499
|
+
receiveHead(connection, stream, frame) {
|
|
500
|
+
if (!stream.response || stream.receivedHeaders)
|
|
501
|
+
throw new Error('Unexpected response');
|
|
502
|
+
const headers = sanitizeResponseHeaders(frame.headers);
|
|
503
|
+
if (stream.external) {
|
|
504
|
+
// Decided on the head, before any body byte. The tunnel client carries the app's validated
|
|
505
|
+
// content-length on the frame; sanitising has already removed it from what the caller gets.
|
|
506
|
+
const declared = declaredLength(frame.headers['content-length']);
|
|
507
|
+
if (!stream.external.writeHead({ status: frame.status, headers, declaredLength: declared })) {
|
|
508
|
+
this.abort(connection, frame.id, 502);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
// The body may not outgrow the length the head was judged on.
|
|
512
|
+
stream.external.declaredLength = declared;
|
|
513
|
+
stream.receivedHeaders = true;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
stream.receivedHeaders = true;
|
|
517
|
+
stream.response.writeHead(frame.status, developmentResponseHeaders({ response: stream.response, headers }));
|
|
518
|
+
}
|
|
519
|
+
async receiveBody(connection, stream, frame) {
|
|
520
|
+
if (!stream.response || !stream.receivedHeaders || stream.request.method === 'HEAD')
|
|
521
|
+
throw new Error('Unexpected response body');
|
|
522
|
+
const bytes = Buffer.from(frame.data, 'base64');
|
|
523
|
+
stream.bytes += bytes.length;
|
|
524
|
+
const cap = stream.external
|
|
525
|
+
? Math.min(stream.external.maxResponseBytes, stream.external.declaredLength ?? Number.POSITIVE_INFINITY)
|
|
526
|
+
: this.limits.maxBodyBytes;
|
|
527
|
+
if (stream.bytes > cap) {
|
|
528
|
+
this.abort(connection, frame.id, 502);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (stream.external)
|
|
532
|
+
await this.writeExternalChunk(connection, frame.id, stream.response, bytes, stream.external.idleWriteTimeoutMs);
|
|
533
|
+
else
|
|
534
|
+
await writeChunk(stream.response, bytes);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Backpressure from an external caller abandons the request after the idle timeout instead of
|
|
538
|
+
* holding its stream (and the caller's admission permit) until the request timeout. A write the
|
|
539
|
+
* abort interrupted is not a tunnel failure, so the session's other streams carry on.
|
|
540
|
+
*/
|
|
541
|
+
async writeExternalChunk(connection, id, response, bytes, idleWriteTimeoutMs) {
|
|
542
|
+
const timer = setTimeout(() => this.abort(connection, id, 504), idleWriteTimeoutMs);
|
|
543
|
+
timer.unref();
|
|
544
|
+
try {
|
|
545
|
+
await writeChunk(response, bytes);
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
if (connection.streams.has(id))
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
clearTimeout(timer);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
402
555
|
finish(connection, id, status) {
|
|
403
556
|
const stream = connection.streams.get(id);
|
|
404
557
|
if (!stream)
|
|
@@ -408,8 +561,13 @@ export class ArtifactTunnelBroker {
|
|
|
408
561
|
clearTimeout(stream.timer);
|
|
409
562
|
stream.message.reset();
|
|
410
563
|
if (stream.response) {
|
|
411
|
-
if (status || !stream.receivedHeaders)
|
|
412
|
-
|
|
564
|
+
if (status || !stream.receivedHeaders) {
|
|
565
|
+
const failure = status ?? 502;
|
|
566
|
+
if (stream.external)
|
|
567
|
+
stream.external.fail(isExternalFailure(failure) ? failure : 502);
|
|
568
|
+
else
|
|
569
|
+
rejectHttp(stream.response, failure);
|
|
570
|
+
}
|
|
413
571
|
else
|
|
414
572
|
stream.response.end();
|
|
415
573
|
}
|
package/dist/tunnel/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type TunnelLimits } from './peer.js';
|
|
2
|
+
import { type TunnelFrame } from './protocol.js';
|
|
2
3
|
export interface LocalTunnelOptions {
|
|
3
4
|
url: string;
|
|
4
5
|
token: string;
|
|
@@ -20,3 +21,15 @@ export interface LocalTunnelConnection {
|
|
|
20
21
|
/** One connection/epoch only. The caller obtains a fresh lease to reconnect;
|
|
21
22
|
* no HTTP request or mutation is ever retained or replayed. */
|
|
22
23
|
export declare function connectLocalTunnel(options: LocalTunnelOptions): Promise<LocalTunnelConnection>;
|
|
24
|
+
/**
|
|
25
|
+
* The origin a forwarded request is pinned to. An external ingress request (its context signed
|
|
26
|
+
* by an `external-` kid, which only the gateway puts on a frame) keeps the ingress origin the
|
|
27
|
+
* broker set, so the app sees the same host as on a hosted release. Everything else is pinned to
|
|
28
|
+
* this session's own origin.
|
|
29
|
+
*/
|
|
30
|
+
export declare function tunnelFrameOrigin({ headers, sessionOrigin }: {
|
|
31
|
+
headers: Extract<TunnelFrame, {
|
|
32
|
+
type: 'request';
|
|
33
|
+
}>['headers'];
|
|
34
|
+
sessionOrigin: string;
|
|
35
|
+
}): string;
|
package/dist/tunnel/client.js
CHANGED
|
@@ -3,7 +3,9 @@ import WebSocket from 'ws';
|
|
|
3
3
|
import { TunnelPeer, tunnelLimits } from './peer.js';
|
|
4
4
|
import { assertSafeRequest, normalizePublicOrigin, parseTunnelFrame, sanitizeRequestHeaders, sanitizeResponseHeaders, TUNNEL_PROTOCOL } from './protocol.js';
|
|
5
5
|
import { forwardWebSocketMessages, MessageBudget, permittedCloseCode, sendWebSocket, WebSocketMessage, writeChunk } from './io.js';
|
|
6
|
+
import { declaredLength } from './external.js';
|
|
6
7
|
import { isLoopbackHostname } from '../loopback.js';
|
|
8
|
+
import { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX } from '../server/internal/signed-token.js';
|
|
7
9
|
/** One connection/epoch only. The caller obtains a fresh lease to reconnect;
|
|
8
10
|
* no HTTP request or mutation is ever retained or replayed. */
|
|
9
11
|
export async function connectLocalTunnel(options) {
|
|
@@ -52,7 +54,7 @@ export async function connectLocalTunnel(options) {
|
|
|
52
54
|
const handle = async (frame) => {
|
|
53
55
|
if (frame.type === 'request') {
|
|
54
56
|
assertSafeRequest(frame.method, frame.path);
|
|
55
|
-
const headers = sanitizeRequestHeaders(frame.headers, publicOrigin);
|
|
57
|
+
const headers = sanitizeRequestHeaders(frame.headers, tunnelFrameOrigin({ headers: frame.headers, sessionOrigin: publicOrigin }));
|
|
56
58
|
// Only the already authenticated gateway may mint this signed context.
|
|
57
59
|
const context = frame.headers['x-sequence-artifact-context'];
|
|
58
60
|
if (typeof context === 'string')
|
|
@@ -91,7 +93,14 @@ export async function connectLocalTunnel(options) {
|
|
|
91
93
|
local.once('response', (response) => {
|
|
92
94
|
void (async () => {
|
|
93
95
|
try {
|
|
94
|
-
|
|
96
|
+
// Sanitising drops content-length as a hop header. The validated length the app declared
|
|
97
|
+
// rides on the frame so the gateway can decide an external response on its head as hosted
|
|
98
|
+
// ingress does (an empty 200 needs no content type); the broker never passes it on.
|
|
99
|
+
const declared = declaredLength(response.headers['content-length']);
|
|
100
|
+
const headers = sanitizeResponseHeaders(response.headers);
|
|
101
|
+
if (declared !== null)
|
|
102
|
+
headers['content-length'] = String(declared);
|
|
103
|
+
await peer.send({ type: 'response', id: frame.id, status: response.statusCode ?? 502, headers });
|
|
95
104
|
let total = 0;
|
|
96
105
|
for await (const chunk of response) {
|
|
97
106
|
const bytes = Buffer.from(chunk);
|
|
@@ -182,3 +191,32 @@ export async function connectLocalTunnel(options) {
|
|
|
182
191
|
});
|
|
183
192
|
return { closed, close: abort };
|
|
184
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* The origin a forwarded request is pinned to. An external ingress request (its context signed
|
|
196
|
+
* by an `external-` kid, which only the gateway puts on a frame) keeps the ingress origin the
|
|
197
|
+
* broker set, so the app sees the same host as on a hosted release. Everything else is pinned to
|
|
198
|
+
* this session's own origin.
|
|
199
|
+
*/
|
|
200
|
+
export function tunnelFrameOrigin({ headers, sessionOrigin }) {
|
|
201
|
+
const context = headers['x-sequence-artifact-context'];
|
|
202
|
+
const host = headers['x-forwarded-host'];
|
|
203
|
+
const proto = headers['x-forwarded-proto'];
|
|
204
|
+
if (typeof context !== 'string' || typeof host !== 'string' || typeof proto !== 'string' || !signedByExternalKid(context))
|
|
205
|
+
return sessionOrigin;
|
|
206
|
+
try {
|
|
207
|
+
return normalizePublicOrigin(`${proto}://${host}`);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return sessionOrigin;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function signedByExternalKid(token) {
|
|
214
|
+
try {
|
|
215
|
+
const header = JSON.parse(Buffer.from(token.split('.')[0] ?? '', 'base64url').toString('utf8'));
|
|
216
|
+
return typeof header === 'object' && header !== null && 'kid' in header && typeof header.kid === 'string' &&
|
|
217
|
+
header.kid.startsWith(ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
/** The failures an external request's writer answers itself (the gateway's error contract). */
|
|
3
|
+
export type TunnelExternalFailure = 400 | 413 | 502 | 503 | 504;
|
|
4
|
+
/**
|
|
5
|
+
* An external ingress request proxied over a development session's tunnel (local development
|
|
6
|
+
* only, design §(j.3) step 3). No browser session or cookie is involved: the gateway admitted the
|
|
7
|
+
* caller and signed the context. The broker only checks that the session's tunnel belongs to the
|
|
8
|
+
* granted project and environment, forwards ingress-sanitised headers, and leaves every response
|
|
9
|
+
* decision to the caller's `writeHead` and every failure to its `fail`.
|
|
10
|
+
*/
|
|
11
|
+
export interface TunnelExternalRequest {
|
|
12
|
+
request: IncomingMessage;
|
|
13
|
+
response: ServerResponse;
|
|
14
|
+
sessionId: string;
|
|
15
|
+
projectId: string;
|
|
16
|
+
environment: string;
|
|
17
|
+
/**
|
|
18
|
+
* The ingress host's canonical origin. The app sees it as `host` / `x-forwarded-host`, as it sees
|
|
19
|
+
* the ingress origin on a hosted release, not the development session's own origin.
|
|
20
|
+
*/
|
|
21
|
+
publicOrigin: string;
|
|
22
|
+
/** The signed external context, forwarded as the only platform header. */
|
|
23
|
+
requestContext: string;
|
|
24
|
+
/** Declared request headers forwarded on top of the fixed ingress allowlist. */
|
|
25
|
+
appHeaders: readonly string[];
|
|
26
|
+
/** The request body, when the caller buffered it; otherwise the request stream is forwarded. */
|
|
27
|
+
body?: AsyncIterable<Uint8Array>;
|
|
28
|
+
maxRequestBytes: number;
|
|
29
|
+
maxResponseBytes: number;
|
|
30
|
+
timeoutMs: number;
|
|
31
|
+
/** A response whose writes stay blocked this long is abandoned (504). */
|
|
32
|
+
idleWriteTimeoutMs: number;
|
|
33
|
+
/** Applies the response policy to the head and sets it; false refuses the response (502, no body byte sent). */
|
|
34
|
+
writeHead(input: {
|
|
35
|
+
status: number;
|
|
36
|
+
headers: Record<string, string | string[]>;
|
|
37
|
+
declaredLength: number | null;
|
|
38
|
+
}): boolean;
|
|
39
|
+
/** Writes a broker-decided failure; destroys the response when its head was already sent. */
|
|
40
|
+
fail(status: TunnelExternalFailure): void;
|
|
41
|
+
}
|
|
42
|
+
export type ExternalStream = Pick<TunnelExternalRequest, 'writeHead' | 'fail' | 'maxResponseBytes' | 'idleWriteTimeoutMs'> & {
|
|
43
|
+
/** The content-length the accepted head declared; the body is cut off once it passes it. */
|
|
44
|
+
declaredLength?: number | null;
|
|
45
|
+
};
|
|
46
|
+
export declare function isExternalFailure(status: number): status is TunnelExternalFailure;
|
|
47
|
+
/**
|
|
48
|
+
* A declared body length as a plain decimal, else null. Read from a raw head: the shared
|
|
49
|
+
* sanitising removes `content-length` as a hop header.
|
|
50
|
+
*/
|
|
51
|
+
export declare function declaredLength(value: string | string[] | undefined): number | null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function isExternalFailure(status) {
|
|
2
|
+
return status === 400 || status === 413 || status === 502 || status === 503 || status === 504;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* A declared body length as a plain decimal, else null. Read from a raw head: the shared
|
|
6
|
+
* sanitising removes `content-length` as a hop header.
|
|
7
|
+
*/
|
|
8
|
+
export function declaredLength(value) {
|
|
9
|
+
return typeof value === 'string' && /^(0|[1-9][0-9]*)$/.test(value) ? Number(value) : null;
|
|
10
|
+
}
|
package/dist/tunnel/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { connectLocalTunnel, type LocalTunnelOptions, type LocalTunnelConnection } from './client.js';
|
|
2
|
-
export { ArtifactTunnelBroker, SingleProcessTunnelOwnership, type TunnelBrokerOptions, type TunnelLease, type TunnelOwner, type TunnelOwnershipStore, type GatewayAuthorization, } from './broker.js';
|
|
2
|
+
export { ArtifactTunnelBroker, SingleProcessTunnelOwnership, type TunnelBrokerOptions, type TunnelLease, type TunnelOwner, type TunnelOwnershipStore, type GatewayAuthorization, type TunnelExternalFailure, type TunnelExternalRequest, } from './broker.js';
|
|
3
3
|
export { DEFAULT_TUNNEL_LIMITS, TUNNEL_PROTOCOL, type TunnelFrame } from './protocol.js';
|
|
4
4
|
export type { TunnelLimits } from './peer.js';
|
|
5
5
|
export type { TunnelRelay, TunnelSocket } from './transport.js';
|
|
@@ -63,8 +63,26 @@ export type TunnelFrame = z.infer<typeof tunnelFrameSchema>;
|
|
|
63
63
|
export declare function parseTunnelFrame(input: string | Buffer | ArrayBuffer | Buffer[]): TunnelFrame;
|
|
64
64
|
export declare function encodeTunnelFrame(frame: TunnelFrame): string;
|
|
65
65
|
export declare function normalizePublicOrigin(value: string): string;
|
|
66
|
-
/**
|
|
67
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The request headers an external ingress forwards (design §(e) "Request sanitising"): a short
|
|
68
|
+
* fixed set plus the `x-` headers the ingress declared in its manifest. Everything else is
|
|
69
|
+
* dropped, so a cross-origin caller cannot reach framework-internal, routing-override,
|
|
70
|
+
* conditional or client-address headers the app might trust.
|
|
71
|
+
*/
|
|
72
|
+
export declare const INGRESS_FORWARDED_REQUEST_HEADERS: readonly string[];
|
|
73
|
+
/** `session`: same-origin iframe traffic. `ingress`: an external caller; `appHeaders` are the ingress's declared headers. */
|
|
74
|
+
export type RequestHeaderMode = {
|
|
75
|
+
mode?: 'session';
|
|
76
|
+
} | {
|
|
77
|
+
mode: 'ingress';
|
|
78
|
+
appHeaders: readonly string[];
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Rebuild trusted routing headers; no Atlas bearer/session crosses into app code. A session
|
|
82
|
+
* request must come from its own origin; an `ingress` request is cross-origin by design, so it
|
|
83
|
+
* keeps only its allowlisted headers and its `Origin` is dropped instead.
|
|
84
|
+
*/
|
|
85
|
+
export declare function sanitizeRequestHeaders(headers: HeaderInput, publicOrigin: string, mode?: RequestHeaderMode): TunnelHeaders;
|
|
68
86
|
/** Response headers through which application output could opt into CDN or proxy reuse. */
|
|
69
87
|
export declare function isSharedCacheDirective(name: string): boolean;
|
|
70
88
|
/** Sent in their place on every protected response, so each reuse must pass gateway authorization again. */
|
package/dist/tunnel/protocol.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { isLoopbackHostname } from '../loopback.js';
|
|
4
|
-
import { isPrivateRequestHeader } from './reserved-headers.js';
|
|
4
|
+
import { INGRESS_CLIENT_ADDRESS_HEADERS, isPrivateRequestHeader } from './reserved-headers.js';
|
|
5
5
|
/** Version 1 is negotiated by the tunnel connection, not by individual streams. */
|
|
6
6
|
export const TUNNEL_PROTOCOL = 'artifact-tunnel.v1';
|
|
7
7
|
export const DEFAULT_TUNNEL_LIMITS = Object.freeze({
|
|
@@ -148,13 +148,49 @@ function removePrivateHeaders(headers) {
|
|
|
148
148
|
headers.delete(name);
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
/**
|
|
152
|
-
|
|
151
|
+
/**
|
|
152
|
+
* The request headers an external ingress forwards (design §(e) "Request sanitising"): a short
|
|
153
|
+
* fixed set plus the `x-` headers the ingress declared in its manifest. Everything else is
|
|
154
|
+
* dropped, so a cross-origin caller cannot reach framework-internal, routing-override,
|
|
155
|
+
* conditional or client-address headers the app might trust.
|
|
156
|
+
*/
|
|
157
|
+
export const INGRESS_FORWARDED_REQUEST_HEADERS = Object.freeze([
|
|
158
|
+
// Content-Length is a hop header the proxy re-frames, so it is not listed.
|
|
159
|
+
'content-type', 'accept', 'accept-encoding', 'user-agent',
|
|
160
|
+
]);
|
|
161
|
+
/**
|
|
162
|
+
* Dropped on an ingress even if an ingress declared one (the manifest already refuses them):
|
|
163
|
+
* browser credentials and the caller's `Origin` (the gateway matched it against the binding),
|
|
164
|
+
* the edge secret, and every client-address header. The client address an app sees comes only
|
|
165
|
+
* from the signed context, never from a header the caller or another Cloudflare zone could set.
|
|
166
|
+
*/
|
|
167
|
+
const INGRESS_DROPPED_REQUEST_HEADERS = new Set([
|
|
168
|
+
'cookie', 'origin', 'authorization', 'x-sequence-edge', ...INGRESS_CLIENT_ADDRESS_HEADERS,
|
|
169
|
+
]);
|
|
170
|
+
function isIngressForwardedRequestHeader(name, declared) {
|
|
171
|
+
if (INGRESS_DROPPED_REQUEST_HEADERS.has(name) || name.startsWith('cf-'))
|
|
172
|
+
return false;
|
|
173
|
+
return INGRESS_FORWARDED_REQUEST_HEADERS.includes(name) || declared.has(name);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Rebuild trusted routing headers; no Atlas bearer/session crosses into app code. A session
|
|
177
|
+
* request must come from its own origin; an `ingress` request is cross-origin by design, so it
|
|
178
|
+
* keeps only its allowlisted headers and its `Origin` is dropped instead.
|
|
179
|
+
*/
|
|
180
|
+
export function sanitizeRequestHeaders(headers, publicOrigin, mode = {}) {
|
|
153
181
|
const origin = normalizePublicOrigin(publicOrigin);
|
|
154
182
|
const normalized = normalizedHeaders(headers, browserRequestHeadersSchema);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
183
|
+
if (mode.mode === 'ingress') {
|
|
184
|
+
const declared = new Set(mode.appHeaders);
|
|
185
|
+
for (const name of [...normalized.keys()])
|
|
186
|
+
if (!isIngressForwardedRequestHeader(name, declared))
|
|
187
|
+
normalized.delete(name);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
const suppliedOrigin = normalized.get('origin');
|
|
191
|
+
if (suppliedOrigin !== undefined && suppliedOrigin !== origin)
|
|
192
|
+
throw new Error('Tunnel request origin mismatch');
|
|
193
|
+
}
|
|
158
194
|
removePrivateHeaders(normalized);
|
|
159
195
|
const cookie = normalized.get('cookie');
|
|
160
196
|
if (cookie !== undefined) {
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"publishConfig": {
|
|
10
10
|
"access": "public"
|
|
11
11
|
},
|
|
12
|
-
"version": "0.6.
|
|
12
|
+
"version": "0.6.6",
|
|
13
13
|
"description": "SDK types and CLI library for building Artifact Studio apps. Driven via `seq-studio artifact <sub>` (@sequenceholdings/studio-cli), which runs the `./cli` runCli export.",
|
|
14
14
|
"type": "module",
|
|
15
15
|
"exports": {
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"VERSION-PIN.md"
|
|
149
149
|
],
|
|
150
150
|
"dependencies": {
|
|
151
|
-
"@sequenceholdings/atlas-ui": "3.
|
|
151
|
+
"@sequenceholdings/atlas-ui": "3.4.1",
|
|
152
152
|
"@tailwindcss/postcss": "^4.1.8",
|
|
153
153
|
"@tailwindcss/vite": "^4.1.8",
|
|
154
154
|
"@tanstack/react-query": "^4.36.1",
|
|
@@ -167,8 +167,8 @@
|
|
|
167
167
|
"server-only": "0.0.1",
|
|
168
168
|
"parse5": "7.3.0",
|
|
169
169
|
"@sequenceholdings/lattice-form-renderer": "0.2.0",
|
|
170
|
-
"@sequenceholdings/
|
|
171
|
-
"@sequenceholdings/
|
|
170
|
+
"@sequenceholdings/orm": "2.5.0",
|
|
171
|
+
"@sequenceholdings/storage": "0.1.1"
|
|
172
172
|
},
|
|
173
173
|
"devDependencies": {
|
|
174
174
|
"@types/js-yaml": "^4.0.9",
|