@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,451 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import { TunnelPeer, tunnelLimits } from './peer.js';
|
|
3
|
+
import { assertSafeRequest, encodeTunnelFrame, normalizePublicOrigin, sanitizeRequestHeaders, sanitizeResponseHeaders, TUNNEL_PROTOCOL } from './protocol.js';
|
|
4
|
+
import { forwardWebSocketMessages, MessageBudget, permittedCloseCode, sendWebSocket, WebSocketMessage, writeChunk } from './io.js';
|
|
5
|
+
function rejectUpgrade(socket, status) {
|
|
6
|
+
socket.end(`HTTP/1.1 ${status} ${status === 403 ? 'Forbidden' : 'Service Unavailable'}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
|
|
7
|
+
}
|
|
8
|
+
function rejectHttp(response, status) {
|
|
9
|
+
if (response.headersSent) {
|
|
10
|
+
response.destroy();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
response.writeHead(status, { 'cache-control': 'no-store', 'content-type': 'application/json' });
|
|
14
|
+
response.end(JSON.stringify({ error: status === 403 ? 'Preview access denied' : 'Development server unavailable' }));
|
|
15
|
+
}
|
|
16
|
+
function preserveGatewayCookies({ response, headers }) {
|
|
17
|
+
const gateway = response.getHeader('set-cookie');
|
|
18
|
+
const application = headers['set-cookie'];
|
|
19
|
+
if (gateway === undefined || application === undefined)
|
|
20
|
+
return;
|
|
21
|
+
// Reserved names were removed from application cookies by the sanitizer.
|
|
22
|
+
// Use a new array: appendHeader mutates the gateway's existing cookie array.
|
|
23
|
+
headers['set-cookie'] = [
|
|
24
|
+
...(Array.isArray(gateway) ? gateway : [String(gateway)]),
|
|
25
|
+
...(Array.isArray(application) ? application : [application]),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
/** Transport only: all identity, cookie bootstrap and FGA checks belong to the
|
|
29
|
+
* mandatory Atlas callbacks. An unconfigured broker cannot accept connections. */
|
|
30
|
+
export class ArtifactTunnelBroker {
|
|
31
|
+
options;
|
|
32
|
+
connections = new Map();
|
|
33
|
+
limits;
|
|
34
|
+
tunnels;
|
|
35
|
+
pendingConnections = new Map();
|
|
36
|
+
ownerTimers = new Set();
|
|
37
|
+
closing = false;
|
|
38
|
+
constructor(options) {
|
|
39
|
+
this.options = options;
|
|
40
|
+
if (!options.nodeId || !options.routing || !options.authorizeTunnel || !options.authorizeRequest || !options.validateLease) {
|
|
41
|
+
throw new Error('Atlas authorization and ownership routing are required');
|
|
42
|
+
}
|
|
43
|
+
this.limits = tunnelLimits(options.limits);
|
|
44
|
+
if (Boolean(options.relay) !== Boolean(options.resolveLease))
|
|
45
|
+
throw new Error('Distributed relay requires lease resolution');
|
|
46
|
+
this.tunnels = new WebSocketServer({ noServer: true, maxPayload: this.limits.maxFrameBytes, perMessageDeflate: false });
|
|
47
|
+
}
|
|
48
|
+
async handleTunnelUpgrade(request, socket, head) {
|
|
49
|
+
socket.pause();
|
|
50
|
+
let lease = null;
|
|
51
|
+
let claimed = false;
|
|
52
|
+
try {
|
|
53
|
+
if (this.closing) {
|
|
54
|
+
rejectUpgrade(socket, 503);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (request.headers['sec-websocket-protocol'] !== TUNNEL_PROTOCOL) {
|
|
58
|
+
rejectUpgrade(socket, 403);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
lease = await this.options.authorizeTunnel(request);
|
|
62
|
+
if (!lease || !Number.isSafeInteger(lease.epoch) || lease.epoch < 1 || lease.expiresAt <= Date.now() ||
|
|
63
|
+
!lease.ownerId || !lease.projectId || !lease.environment || !lease.sessionId || !(await this.options.validateLease(lease))) {
|
|
64
|
+
rejectUpgrade(socket, 403);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
lease.publicOrigin = normalizePublicOrigin(lease.publicOrigin);
|
|
68
|
+
if (request.headers['x-artifact-session-id'] !== lease.sessionId || request.headers['x-artifact-tunnel-epoch'] !== String(lease.epoch)) {
|
|
69
|
+
rejectUpgrade(socket, 403);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
claimed = await this.options.routing.claim(lease, this.options.nodeId);
|
|
73
|
+
const owner = await this.options.routing.lookup(lease.sessionId);
|
|
74
|
+
if (!claimed || owner?.nodeId !== this.options.nodeId || owner.epoch !== lease.epoch || socket.destroyed) {
|
|
75
|
+
rejectUpgrade(socket, 403);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const authorizedLease = lease;
|
|
79
|
+
this.tunnels.handleUpgrade(request, socket, head, (websocket) => {
|
|
80
|
+
const ready = () => websocket.send(encodeTunnelFrame({ type: 'ready', id: authorizedLease.sessionId, epoch: authorizedLease.epoch }));
|
|
81
|
+
websocket.once('close', () => this.options.onTunnelClosed?.(authorizedLease));
|
|
82
|
+
if (this.options.relay) {
|
|
83
|
+
void this.options.relay.attach(authorizedLease, websocket).then(() => {
|
|
84
|
+
if (this.closing) {
|
|
85
|
+
websocket.terminate();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let checking = false;
|
|
89
|
+
const timer = setInterval(() => {
|
|
90
|
+
if (authorizedLease.expiresAt <= Date.now()) {
|
|
91
|
+
websocket.terminate();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (checking)
|
|
95
|
+
return;
|
|
96
|
+
checking = true;
|
|
97
|
+
void this.options.validateLease(authorizedLease).then((valid) => {
|
|
98
|
+
if (!valid)
|
|
99
|
+
websocket.terminate();
|
|
100
|
+
}).catch(() => websocket.terminate()).finally(() => { checking = false; });
|
|
101
|
+
}, Math.min(5_000, Math.max(1, authorizedLease.expiresAt - Date.now())));
|
|
102
|
+
timer.unref();
|
|
103
|
+
this.ownerTimers.add(timer);
|
|
104
|
+
websocket.once('close', () => { clearInterval(timer); this.ownerTimers.delete(timer); });
|
|
105
|
+
ready();
|
|
106
|
+
}).catch(() => websocket.terminate());
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.attach(authorizedLease, websocket);
|
|
110
|
+
ready();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
socket.resume();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
rejectUpgrade(socket, 403);
|
|
117
|
+
if (claimed && lease)
|
|
118
|
+
await this.options.routing.release(lease.sessionId, lease.epoch, this.options.nodeId).catch(() => undefined);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async handleHttp(request, response, sessionId) {
|
|
122
|
+
let connection;
|
|
123
|
+
let id;
|
|
124
|
+
try {
|
|
125
|
+
const authorized = await this.authorize(request, sessionId);
|
|
126
|
+
if (!authorized) {
|
|
127
|
+
rejectHttp(response, 403);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
connection = authorized.connection;
|
|
131
|
+
if (!connection) {
|
|
132
|
+
rejectHttp(response, 503);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
id = connection.peer.open();
|
|
136
|
+
const streamId = id;
|
|
137
|
+
const active = connection;
|
|
138
|
+
const timer = setTimeout(() => this.abort(active, streamId, 504), this.limits.requestTimeoutMs);
|
|
139
|
+
timer.unref();
|
|
140
|
+
connection.streams.set(id, {
|
|
141
|
+
request, response, timer, bytes: 0, receivedHeaders: false,
|
|
142
|
+
message: new WebSocketMessage(this.limits.maxWebSocketMessageBytes, connection.messageBudget),
|
|
143
|
+
});
|
|
144
|
+
response.once('close', () => { if (!response.writableFinished)
|
|
145
|
+
this.abort(active, streamId); });
|
|
146
|
+
request.once('aborted', () => this.abort(active, streamId));
|
|
147
|
+
await connection.peer.send({ type: 'request', id, method: request.method ?? 'GET', path: request.url ?? '/', headers: authorized.headers, websocket: false });
|
|
148
|
+
let total = 0;
|
|
149
|
+
for await (const chunk of request) {
|
|
150
|
+
const bytes = Buffer.from(chunk);
|
|
151
|
+
total += bytes.length;
|
|
152
|
+
if (total > this.limits.maxBodyBytes) {
|
|
153
|
+
this.abort(connection, id, 413);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
await connection.peer.data(id, bytes);
|
|
157
|
+
}
|
|
158
|
+
if (connection.streams.has(id))
|
|
159
|
+
await connection.peer.send({ type: 'end', id });
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
if (connection && id)
|
|
163
|
+
this.abort(connection, id);
|
|
164
|
+
else
|
|
165
|
+
rejectHttp(response, 403);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async handleWebSocketUpgrade(request, socket, head, sessionId) {
|
|
169
|
+
socket.pause();
|
|
170
|
+
try {
|
|
171
|
+
const authorized = await this.authorize(request, sessionId);
|
|
172
|
+
if (!authorized) {
|
|
173
|
+
rejectUpgrade(socket, 403);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const connection = authorized.connection;
|
|
177
|
+
if (!connection) {
|
|
178
|
+
rejectUpgrade(socket, 503);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const id = connection.peer.open();
|
|
182
|
+
const timer = setTimeout(() => this.abort(connection, id), this.limits.requestTimeoutMs);
|
|
183
|
+
timer.unref();
|
|
184
|
+
connection.streams.set(id, {
|
|
185
|
+
request, upgrade: { socket, head }, timer, bytes: 0, receivedHeaders: false,
|
|
186
|
+
message: new WebSocketMessage(this.limits.maxWebSocketMessageBytes, connection.messageBudget),
|
|
187
|
+
});
|
|
188
|
+
socket.once('close', () => this.abort(connection, id));
|
|
189
|
+
await connection.peer.send({ type: 'request', id, method: 'GET', path: request.url ?? '/', headers: authorized.headers, websocket: true });
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
rejectUpgrade(socket, 403);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async authorize(request, sessionId) {
|
|
196
|
+
assertSafeRequest(request.method ?? 'GET', request.url ?? '/');
|
|
197
|
+
const authorization = await this.options.authorizeRequest(request, sessionId);
|
|
198
|
+
if (!authorization)
|
|
199
|
+
return null;
|
|
200
|
+
const origin = normalizePublicOrigin(authorization.publicOrigin);
|
|
201
|
+
if (request.headers.host !== new URL(origin).host)
|
|
202
|
+
return null;
|
|
203
|
+
if (!['GET', 'HEAD', 'OPTIONS'].includes(request.method ?? 'GET') && request.headers.origin !== origin)
|
|
204
|
+
return null;
|
|
205
|
+
const headers = sanitizeRequestHeaders(request.headers, origin);
|
|
206
|
+
if (authorization.requestContext) {
|
|
207
|
+
if (/[\r\n]/.test(authorization.requestContext))
|
|
208
|
+
return null;
|
|
209
|
+
headers['x-sequence-artifact-context'] = authorization.requestContext;
|
|
210
|
+
}
|
|
211
|
+
let connection = this.connections.get(sessionId);
|
|
212
|
+
if (this.options.relay && this.options.resolveLease) {
|
|
213
|
+
const lease = await this.options.resolveLease(sessionId);
|
|
214
|
+
if (!lease)
|
|
215
|
+
return { headers };
|
|
216
|
+
if (lease.ownerId !== authorization.ownerId || normalizePublicOrigin(lease.publicOrigin) !== origin || !(await this.options.validateLease(lease)))
|
|
217
|
+
return null;
|
|
218
|
+
if (!connection || connection.lease.epoch !== lease.epoch)
|
|
219
|
+
connection = await this.remoteConnection(lease);
|
|
220
|
+
else
|
|
221
|
+
connection.lease.expiresAt = lease.expiresAt;
|
|
222
|
+
}
|
|
223
|
+
if (!connection)
|
|
224
|
+
return { headers };
|
|
225
|
+
if (connection.lease.ownerId !== authorization.ownerId || connection.lease.publicOrigin !== origin || !(await this.valid(connection)))
|
|
226
|
+
return null;
|
|
227
|
+
return { connection, headers };
|
|
228
|
+
}
|
|
229
|
+
async valid(connection) {
|
|
230
|
+
const owner = await this.options.routing.lookup(connection.lease.sessionId);
|
|
231
|
+
if (this.options.resolveLease) {
|
|
232
|
+
const lease = await this.options.resolveLease(connection.lease.sessionId);
|
|
233
|
+
if (lease?.epoch === connection.lease.epoch)
|
|
234
|
+
connection.lease.expiresAt = lease.expiresAt;
|
|
235
|
+
}
|
|
236
|
+
return connection.lease.expiresAt > Date.now() && Boolean(owner) && (Boolean(this.options.relay) || owner?.nodeId === this.options.nodeId) && owner?.epoch === connection.lease.epoch &&
|
|
237
|
+
await this.options.validateLease(connection.lease);
|
|
238
|
+
}
|
|
239
|
+
async remoteConnection(lease) {
|
|
240
|
+
const key = `${lease.sessionId}:${lease.epoch}`;
|
|
241
|
+
const pending = this.pendingConnections.get(key);
|
|
242
|
+
if (pending)
|
|
243
|
+
return pending;
|
|
244
|
+
const connecting = (async () => {
|
|
245
|
+
const socket = await this.options.relay.connect(lease);
|
|
246
|
+
if (this.closing) {
|
|
247
|
+
socket.terminate();
|
|
248
|
+
throw new Error('Gateway shutting down');
|
|
249
|
+
}
|
|
250
|
+
const connection = this.attach(lease, socket);
|
|
251
|
+
if (!connection)
|
|
252
|
+
throw new Error('Tunnel lease superseded');
|
|
253
|
+
return connection;
|
|
254
|
+
})();
|
|
255
|
+
this.pendingConnections.set(key, connecting);
|
|
256
|
+
try {
|
|
257
|
+
return await connecting;
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
this.pendingConnections.delete(key);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
attach(lease, socket) {
|
|
264
|
+
const existing = this.connections.get(lease.sessionId);
|
|
265
|
+
if (existing && existing.lease.epoch >= lease.epoch) {
|
|
266
|
+
socket.terminate();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
existing?.peer.close();
|
|
270
|
+
const streams = new Map();
|
|
271
|
+
const peer = new TunnelPeer(socket, this.limits, (frame) => this.receive(connection, frame), () => {
|
|
272
|
+
clearInterval(connection.timer);
|
|
273
|
+
for (const id of streams.keys())
|
|
274
|
+
this.finish(connection, id, 502);
|
|
275
|
+
if (this.connections.get(lease.sessionId) === connection)
|
|
276
|
+
this.connections.delete(lease.sessionId);
|
|
277
|
+
if (!this.options.relay)
|
|
278
|
+
void this.options.routing.release(lease.sessionId, lease.epoch, this.options.nodeId).catch(() => undefined);
|
|
279
|
+
});
|
|
280
|
+
let checking = false;
|
|
281
|
+
const timer = setInterval(() => {
|
|
282
|
+
if (connection.lease.expiresAt <= Date.now()) {
|
|
283
|
+
peer.close();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (checking)
|
|
287
|
+
return;
|
|
288
|
+
checking = true;
|
|
289
|
+
void this.valid(connection).then((valid) => { if (!valid)
|
|
290
|
+
peer.close(); }).catch(() => peer.close()).finally(() => { checking = false; });
|
|
291
|
+
}, Math.min(5_000, Math.max(1, lease.expiresAt - Date.now())));
|
|
292
|
+
timer.unref();
|
|
293
|
+
const connection = { lease, peer, streams, timer, messageBudget: new MessageBudget(this.limits.maxBufferedBytes) };
|
|
294
|
+
this.connections.set(lease.sessionId, connection);
|
|
295
|
+
return connection;
|
|
296
|
+
}
|
|
297
|
+
async receive(connection, frame) {
|
|
298
|
+
const stream = connection.streams.get(frame.id);
|
|
299
|
+
if (!stream)
|
|
300
|
+
return;
|
|
301
|
+
if (frame.type === 'request')
|
|
302
|
+
throw new Error('Developer cannot initiate gateway requests');
|
|
303
|
+
if (frame.type === 'response') {
|
|
304
|
+
if (!stream.response || stream.receivedHeaders)
|
|
305
|
+
throw new Error('Unexpected response');
|
|
306
|
+
stream.receivedHeaders = true;
|
|
307
|
+
const headers = sanitizeResponseHeaders(frame.headers);
|
|
308
|
+
// A development process cannot relax the gateway's embedding policy.
|
|
309
|
+
// Multiple CSP policies are intersected by browsers, preserving app CSP.
|
|
310
|
+
const platformCsp = stream.response.getHeader('content-security-policy');
|
|
311
|
+
const applicationCsp = headers['content-security-policy'];
|
|
312
|
+
if (platformCsp)
|
|
313
|
+
headers['content-security-policy'] = [
|
|
314
|
+
...(Array.isArray(platformCsp) ? platformCsp : [String(platformCsp)]),
|
|
315
|
+
...(applicationCsp === undefined ? [] : Array.isArray(applicationCsp) ? applicationCsp : [applicationCsp]),
|
|
316
|
+
];
|
|
317
|
+
for (const name of ['x-content-type-options', 'referrer-policy']) {
|
|
318
|
+
const value = stream.response.getHeader(name);
|
|
319
|
+
if (value !== undefined)
|
|
320
|
+
headers[name] = String(value);
|
|
321
|
+
}
|
|
322
|
+
headers['cache-control'] = 'no-store';
|
|
323
|
+
preserveGatewayCookies({ response: stream.response, headers });
|
|
324
|
+
stream.response.writeHead(frame.status, headers);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (frame.type === 'data') {
|
|
328
|
+
if (stream.socket) {
|
|
329
|
+
const message = stream.message.accept(frame);
|
|
330
|
+
if (message) {
|
|
331
|
+
try {
|
|
332
|
+
await sendWebSocket(stream.socket, message.bytes, message.binary);
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
stream.message.reset();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
if (!stream.response || !stream.receivedHeaders || stream.request.method === 'HEAD')
|
|
341
|
+
throw new Error('Unexpected response body');
|
|
342
|
+
const bytes = Buffer.from(frame.data, 'base64');
|
|
343
|
+
stream.bytes += bytes.length;
|
|
344
|
+
if (stream.bytes > this.limits.maxBodyBytes) {
|
|
345
|
+
this.abort(connection, frame.id, 502);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
await writeChunk(stream.response, bytes);
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (frame.type === 'end') {
|
|
353
|
+
this.finish(connection, frame.id);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (frame.type === 'cancel') {
|
|
357
|
+
this.finish(connection, frame.id, 502);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (frame.type === 'ws-close') {
|
|
361
|
+
stream.socket?.close(permittedCloseCode(frame.code), frame.reason);
|
|
362
|
+
this.finish(connection, frame.id);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (frame.type === 'ws-open') {
|
|
366
|
+
const upgrade = stream.upgrade;
|
|
367
|
+
if (!upgrade || stream.socket)
|
|
368
|
+
throw new Error('Unexpected websocket upgrade');
|
|
369
|
+
const requested = stream.request.headers['sec-websocket-protocol']?.split(',').map((protocol) => protocol.trim()) ?? [];
|
|
370
|
+
if (frame.protocol && !requested.includes(frame.protocol))
|
|
371
|
+
throw new Error('Unexpected websocket subprotocol');
|
|
372
|
+
const server = new WebSocketServer({
|
|
373
|
+
noServer: true, perMessageDeflate: false, maxPayload: this.limits.maxWebSocketMessageBytes,
|
|
374
|
+
handleProtocols: () => frame.protocol || false,
|
|
375
|
+
});
|
|
376
|
+
server.handleUpgrade(stream.request, upgrade.socket, upgrade.head, (browser) => {
|
|
377
|
+
clearTimeout(stream.timer);
|
|
378
|
+
stream.socket = browser;
|
|
379
|
+
browser.on('error', () => this.abort(connection, frame.id));
|
|
380
|
+
forwardWebSocketMessages(browser, (bytes, binary) => connection.peer.data(frame.id, bytes, { binary }), this.limits.maxBufferedBytes, () => this.abort(connection, frame.id), connection.messageBudget);
|
|
381
|
+
browser.once('close', (code, reason) => {
|
|
382
|
+
void connection.peer.send({ type: 'ws-close', id: frame.id, code: permittedCloseCode(code), reason: reason.toString().slice(0, 100) }).catch(() => undefined);
|
|
383
|
+
this.finish(connection, frame.id);
|
|
384
|
+
server.close();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
upgrade.socket.resume();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
finish(connection, id, status) {
|
|
391
|
+
const stream = connection.streams.get(id);
|
|
392
|
+
if (!stream)
|
|
393
|
+
return;
|
|
394
|
+
connection.streams.delete(id);
|
|
395
|
+
connection.peer.forget(id);
|
|
396
|
+
clearTimeout(stream.timer);
|
|
397
|
+
stream.message.reset();
|
|
398
|
+
if (stream.response) {
|
|
399
|
+
if (status || !stream.receivedHeaders)
|
|
400
|
+
rejectHttp(stream.response, status ?? 502);
|
|
401
|
+
else
|
|
402
|
+
stream.response.end();
|
|
403
|
+
}
|
|
404
|
+
if (stream.socket)
|
|
405
|
+
stream.socket.close(status ? 1011 : 1000);
|
|
406
|
+
else if (stream.upgrade)
|
|
407
|
+
rejectUpgrade(stream.upgrade.socket, status ?? 503);
|
|
408
|
+
}
|
|
409
|
+
abort(connection, id, status = 502) {
|
|
410
|
+
if (!connection.streams.has(id))
|
|
411
|
+
return;
|
|
412
|
+
this.finish(connection, id, status);
|
|
413
|
+
void connection.peer.send({ type: 'cancel', id }).catch(() => undefined);
|
|
414
|
+
}
|
|
415
|
+
/** Immediate revocation and graceful service shutdown hook. */
|
|
416
|
+
disconnect(sessionId) {
|
|
417
|
+
this.connections.get(sessionId)?.peer.close();
|
|
418
|
+
this.options.relay?.disconnect(sessionId);
|
|
419
|
+
}
|
|
420
|
+
close() {
|
|
421
|
+
this.closing = true;
|
|
422
|
+
for (const timer of this.ownerTimers)
|
|
423
|
+
clearInterval(timer);
|
|
424
|
+
this.ownerTimers.clear();
|
|
425
|
+
for (const connection of this.connections.values())
|
|
426
|
+
connection.peer.close();
|
|
427
|
+
this.tunnels.close();
|
|
428
|
+
void this.options.relay?.close().catch(() => undefined);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/** For one-process local development/tests only. Production must provide a shared
|
|
432
|
+
* compare-and-swap store and route ingress to the claimed owner. */
|
|
433
|
+
export class SingleProcessTunnelOwnership {
|
|
434
|
+
owners = new Map();
|
|
435
|
+
async claim(lease, nodeId) {
|
|
436
|
+
const current = this.owners.get(lease.sessionId);
|
|
437
|
+
if (current && current.epoch >= lease.epoch)
|
|
438
|
+
return false;
|
|
439
|
+
this.owners.set(lease.sessionId, { nodeId, epoch: lease.epoch, expiresAt: lease.expiresAt, active: true });
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
async lookup(sessionId) {
|
|
443
|
+
const current = this.owners.get(sessionId);
|
|
444
|
+
return current?.active && current.expiresAt > Date.now() ? current : null;
|
|
445
|
+
}
|
|
446
|
+
async release(sessionId, epoch, nodeId) {
|
|
447
|
+
const current = this.owners.get(sessionId);
|
|
448
|
+
if (current?.epoch === epoch && current.nodeId === nodeId)
|
|
449
|
+
current.active = false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type TunnelLimits } from './peer.js';
|
|
2
|
+
export interface LocalTunnelOptions {
|
|
3
|
+
url: string;
|
|
4
|
+
token: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
epoch: number;
|
|
7
|
+
localPort: number;
|
|
8
|
+
publicOrigin: string;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
allowInsecureLoopback?: boolean;
|
|
11
|
+
limits?: Partial<TunnelLimits>;
|
|
12
|
+
}
|
|
13
|
+
export interface LocalTunnelConnection {
|
|
14
|
+
closed: Promise<{
|
|
15
|
+
code: number;
|
|
16
|
+
reason: string;
|
|
17
|
+
}>;
|
|
18
|
+
close(): void;
|
|
19
|
+
}
|
|
20
|
+
/** One connection/epoch only. The caller obtains a fresh lease to reconnect;
|
|
21
|
+
* no HTTP request or mutation is ever retained or replayed. */
|
|
22
|
+
export declare function connectLocalTunnel(options: LocalTunnelOptions): Promise<LocalTunnelConnection>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { request } from 'node:http';
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
import { TunnelPeer, tunnelLimits } from './peer.js';
|
|
4
|
+
import { assertSafeRequest, normalizePublicOrigin, parseTunnelFrame, sanitizeRequestHeaders, sanitizeResponseHeaders, TUNNEL_PROTOCOL } from './protocol.js';
|
|
5
|
+
import { forwardWebSocketMessages, MessageBudget, permittedCloseCode, sendWebSocket, WebSocketMessage, writeChunk } from './io.js';
|
|
6
|
+
/** One connection/epoch only. The caller obtains a fresh lease to reconnect;
|
|
7
|
+
* no HTTP request or mutation is ever retained or replayed. */
|
|
8
|
+
export async function connectLocalTunnel(options) {
|
|
9
|
+
const endpoint = new URL(options.url);
|
|
10
|
+
const insecureLocal = endpoint.protocol === 'ws:' && ['127.0.0.1', '[::1]', 'localhost'].includes(endpoint.hostname) && options.allowInsecureLoopback;
|
|
11
|
+
if ((endpoint.protocol !== 'wss:' && !insecureLocal) || endpoint.username || endpoint.password || endpoint.hash) {
|
|
12
|
+
throw new Error('Tunnel endpoint must use authenticated WSS');
|
|
13
|
+
}
|
|
14
|
+
if (!Number.isSafeInteger(options.localPort) || options.localPort < 1 || options.localPort > 65535)
|
|
15
|
+
throw new Error('Invalid loopback port');
|
|
16
|
+
if (!Number.isSafeInteger(options.epoch) || options.epoch < 1 || !options.sessionId || !options.token || /[\r\n]/.test(options.token))
|
|
17
|
+
throw new Error('Invalid tunnel lease');
|
|
18
|
+
const publicOrigin = normalizePublicOrigin(options.publicOrigin);
|
|
19
|
+
const limits = tunnelLimits(options.limits);
|
|
20
|
+
options.signal?.throwIfAborted();
|
|
21
|
+
const socket = new WebSocket(endpoint, [TUNNEL_PROTOCOL], {
|
|
22
|
+
headers: { authorization: `Bearer ${options.token}`, 'x-artifact-session-id': options.sessionId, 'x-artifact-tunnel-epoch': String(options.epoch) },
|
|
23
|
+
maxPayload: limits.maxFrameBytes,
|
|
24
|
+
perMessageDeflate: false,
|
|
25
|
+
followRedirects: false,
|
|
26
|
+
handshakeTimeout: limits.requestTimeoutMs,
|
|
27
|
+
});
|
|
28
|
+
const forwards = new Map();
|
|
29
|
+
const messageBudget = new MessageBudget(limits.maxBufferedBytes);
|
|
30
|
+
const dispose = (id) => {
|
|
31
|
+
const forward = forwards.get(id);
|
|
32
|
+
if (!forward)
|
|
33
|
+
return;
|
|
34
|
+
forwards.delete(id);
|
|
35
|
+
clearTimeout(forward.timer);
|
|
36
|
+
forward.message.reset();
|
|
37
|
+
forward.request?.destroy();
|
|
38
|
+
forward.socket?.terminate();
|
|
39
|
+
peer.forget(id);
|
|
40
|
+
};
|
|
41
|
+
const fail = async (id) => {
|
|
42
|
+
dispose(id);
|
|
43
|
+
await peer.send({ type: 'cancel', id }).catch(() => undefined);
|
|
44
|
+
};
|
|
45
|
+
const handle = async (frame) => {
|
|
46
|
+
if (frame.type === 'request') {
|
|
47
|
+
assertSafeRequest(frame.method, frame.path);
|
|
48
|
+
const headers = sanitizeRequestHeaders(frame.headers, publicOrigin);
|
|
49
|
+
// Only the already authenticated gateway may mint this signed context.
|
|
50
|
+
const context = frame.headers['x-sequence-artifact-context'];
|
|
51
|
+
if (typeof context === 'string')
|
|
52
|
+
headers['x-sequence-artifact-context'] = context;
|
|
53
|
+
const timer = setTimeout(() => { void fail(frame.id); }, limits.requestTimeoutMs);
|
|
54
|
+
timer.unref();
|
|
55
|
+
const forward = { message: new WebSocketMessage(limits.maxWebSocketMessageBytes, messageBudget), bytes: 0, timer };
|
|
56
|
+
forwards.set(frame.id, forward);
|
|
57
|
+
if (frame.websocket) {
|
|
58
|
+
clearTimeout(timer); // HMR is long lived; tunnel lease owns its lifetime.
|
|
59
|
+
const protocolHeader = frame.headers['sec-websocket-protocol'];
|
|
60
|
+
const protocols = typeof protocolHeader === 'string' ? protocolHeader.split(',').map((value) => value.trim()) : [];
|
|
61
|
+
for (const name of Object.keys(headers))
|
|
62
|
+
if (name.startsWith('sec-websocket-'))
|
|
63
|
+
delete headers[name];
|
|
64
|
+
const local = new WebSocket(`ws://127.0.0.1:${options.localPort}${frame.path}`, protocols, {
|
|
65
|
+
headers, maxPayload: limits.maxWebSocketMessageBytes, perMessageDeflate: false,
|
|
66
|
+
followRedirects: false, handshakeTimeout: limits.requestTimeoutMs,
|
|
67
|
+
});
|
|
68
|
+
forward.socket = local;
|
|
69
|
+
local.on('error', () => { void fail(frame.id); });
|
|
70
|
+
local.once('open', () => {
|
|
71
|
+
void peer.send({ type: 'ws-open', id: frame.id, protocol: local.protocol }).catch(() => fail(frame.id));
|
|
72
|
+
});
|
|
73
|
+
forwardWebSocketMessages(local, (bytes, binary) => peer.data(frame.id, bytes, { binary }), limits.maxBufferedBytes, () => { void fail(frame.id); }, messageBudget);
|
|
74
|
+
local.once('close', (code, reason) => {
|
|
75
|
+
if (!forwards.has(frame.id))
|
|
76
|
+
return;
|
|
77
|
+
void peer.send({ type: 'ws-close', id: frame.id, code: permittedCloseCode(code), reason: reason.toString().slice(0, 100) }).finally(() => dispose(frame.id)).catch(() => undefined);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const local = request({ hostname: '127.0.0.1', port: options.localPort, method: frame.method, path: frame.path, headers, agent: false });
|
|
82
|
+
forward.request = local;
|
|
83
|
+
local.on('error', () => { void fail(frame.id); });
|
|
84
|
+
local.once('response', (response) => {
|
|
85
|
+
void (async () => {
|
|
86
|
+
try {
|
|
87
|
+
await peer.send({ type: 'response', id: frame.id, status: response.statusCode ?? 502, headers: sanitizeResponseHeaders(response.headers) });
|
|
88
|
+
let total = 0;
|
|
89
|
+
for await (const chunk of response) {
|
|
90
|
+
const bytes = Buffer.from(chunk);
|
|
91
|
+
total += bytes.length;
|
|
92
|
+
if (total > limits.maxBodyBytes)
|
|
93
|
+
throw new Error('Response too large');
|
|
94
|
+
await peer.data(frame.id, bytes);
|
|
95
|
+
}
|
|
96
|
+
await peer.send({ type: 'end', id: frame.id });
|
|
97
|
+
dispose(frame.id);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
await fail(frame.id);
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const forward = forwards.get(frame.id);
|
|
108
|
+
if (!forward)
|
|
109
|
+
return;
|
|
110
|
+
if (frame.type === 'cancel') {
|
|
111
|
+
dispose(frame.id);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (frame.type === 'end') {
|
|
115
|
+
forward.request?.end();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (frame.type === 'ws-close') {
|
|
119
|
+
forward.socket?.close(permittedCloseCode(frame.code), frame.reason);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (frame.type === 'data') {
|
|
123
|
+
if (forward.socket) {
|
|
124
|
+
const message = forward.message.accept(frame);
|
|
125
|
+
if (message) {
|
|
126
|
+
try {
|
|
127
|
+
await sendWebSocket(forward.socket, message.bytes, message.binary);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
forward.message.reset();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (forward.request) {
|
|
135
|
+
const bytes = Buffer.from(frame.data, 'base64');
|
|
136
|
+
forward.bytes += bytes.length;
|
|
137
|
+
if (forward.bytes > limits.maxBodyBytes) {
|
|
138
|
+
await fail(frame.id);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
await writeChunk(forward.request, bytes);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const peer = new TunnelPeer(socket, limits, handle, () => { for (const id of forwards.keys())
|
|
146
|
+
dispose(id); }, true);
|
|
147
|
+
const abort = () => peer.close();
|
|
148
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
149
|
+
const closed = new Promise((resolve) => {
|
|
150
|
+
socket.once('close', (code, reason) => {
|
|
151
|
+
options.signal?.removeEventListener('abort', abort);
|
|
152
|
+
resolve({ code, reason: reason.toString() });
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
await new Promise((resolve, reject) => {
|
|
156
|
+
const ready = (raw) => {
|
|
157
|
+
try {
|
|
158
|
+
const frame = parseTunnelFrame(raw);
|
|
159
|
+
if (frame.type !== 'ready' || frame.id !== options.sessionId || frame.epoch !== options.epoch)
|
|
160
|
+
return;
|
|
161
|
+
cleanup();
|
|
162
|
+
resolve();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
failed();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const failed = () => { cleanup(); reject(new Error('Tunnel connection failed')); };
|
|
169
|
+
const timer = setTimeout(() => { failed(); peer.close(); }, limits.requestTimeoutMs);
|
|
170
|
+
timer.unref();
|
|
171
|
+
const cleanup = () => { clearTimeout(timer); socket.off('message', ready); socket.off('error', failed); socket.off('close', failed); };
|
|
172
|
+
socket.on('message', ready);
|
|
173
|
+
socket.once('error', failed);
|
|
174
|
+
socket.once('close', failed);
|
|
175
|
+
});
|
|
176
|
+
return { closed, close: abort };
|
|
177
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
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';
|
|
3
|
+
export { DEFAULT_TUNNEL_LIMITS, TUNNEL_PROTOCOL, type TunnelFrame } from './protocol.js';
|
|
4
|
+
export type { TunnelLimits } from './peer.js';
|
|
5
|
+
export type { TunnelRelay, TunnelSocket } from './transport.js';
|
|
6
|
+
export { RedisTunnelOwnership, RedisTunnelRelay, type RedisTunnelCommands, type RedisTunnelPublisher, type RedisTunnelSubscriber, type RedisTunnelRelayOptions, } from './redis.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { connectLocalTunnel } from './client.js';
|
|
2
|
+
export { ArtifactTunnelBroker, SingleProcessTunnelOwnership, } from './broker.js';
|
|
3
|
+
export { DEFAULT_TUNNEL_LIMITS, TUNNEL_PROTOCOL } from './protocol.js';
|
|
4
|
+
export { RedisTunnelOwnership, RedisTunnelRelay, } from './redis.js';
|