@sealant/sdk 0.13.5 → 0.14.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/dist/facade/workspace.js +93 -0
- package/dist/types.d.ts +25 -0
- package/package.json +2 -2
package/dist/facade/workspace.js
CHANGED
|
@@ -189,6 +189,99 @@ export const makeWorkspace = (ctx, init) => {
|
|
|
189
189
|
...(ttl === undefined ? {} : { ttlSeconds: ttl === null ? null : parseTtlSeconds(ttl) }),
|
|
190
190
|
}));
|
|
191
191
|
},
|
|
192
|
+
forward: (port) => openForward(ctx, init.id, port),
|
|
192
193
|
};
|
|
193
194
|
return workspace;
|
|
194
195
|
};
|
|
196
|
+
/**
|
|
197
|
+
* Open the held-WebSocket port forward (the byte-pipe data plane, mirroring
|
|
198
|
+
* the session attachment): binary frames are payload bytes in both
|
|
199
|
+
* directions; text frames are control JSON — `{"t":"eof"}` up for half-close,
|
|
200
|
+
* `{"t":"end"}` down when the remote closes. Auth rides the connect
|
|
201
|
+
* (`?token=` / `?ownerUserId=`), never per frame. The server refuses the
|
|
202
|
+
* upgrade with a plain HTTP status when nothing listens on the port, which
|
|
203
|
+
* surfaces here as the connect rejection.
|
|
204
|
+
*/
|
|
205
|
+
const openForward = (ctx, workspaceId, port) => {
|
|
206
|
+
const config = ctx.config;
|
|
207
|
+
const url = new URL(`/v1/workspaces/${workspaceId}/forward`, config.baseUrl);
|
|
208
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
209
|
+
url.searchParams.set("port", String(port));
|
|
210
|
+
if (config.apiKey === undefined) {
|
|
211
|
+
url.searchParams.set("ownerUserId", config.hostLocal.ownerUserId);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
url.searchParams.set("token", config.apiKey);
|
|
215
|
+
}
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
const ws = new WebSocket(url);
|
|
218
|
+
ws.binaryType = "arraybuffer";
|
|
219
|
+
// Push-queue bridging WS message events to the pull-based async iterable.
|
|
220
|
+
const pending = [];
|
|
221
|
+
let wake;
|
|
222
|
+
let finished = false;
|
|
223
|
+
const closedResolver = Promise.withResolvers();
|
|
224
|
+
const closed = closedResolver.promise;
|
|
225
|
+
const finish = (reason) => {
|
|
226
|
+
if (finished) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
finished = true;
|
|
230
|
+
closedResolver.resolve(reason);
|
|
231
|
+
wake?.();
|
|
232
|
+
};
|
|
233
|
+
ws.addEventListener("message", (event) => {
|
|
234
|
+
if (typeof event.data === "string") {
|
|
235
|
+
try {
|
|
236
|
+
const frame = JSON.parse(event.data);
|
|
237
|
+
if (frame.t === "end") {
|
|
238
|
+
finish("end");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Unknown text frame — ignore.
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
pending.push(new Uint8Array(event.data));
|
|
247
|
+
wake?.();
|
|
248
|
+
});
|
|
249
|
+
ws.addEventListener("close", () => finish("closed"));
|
|
250
|
+
const output = {
|
|
251
|
+
[Symbol.asyncIterator]: () => ({
|
|
252
|
+
next: async () => {
|
|
253
|
+
for (;;) {
|
|
254
|
+
const chunk = pending.shift();
|
|
255
|
+
if (chunk !== undefined) {
|
|
256
|
+
return { done: false, value: chunk };
|
|
257
|
+
}
|
|
258
|
+
if (finished) {
|
|
259
|
+
return { done: true, value: undefined };
|
|
260
|
+
}
|
|
261
|
+
await new Promise((r) => {
|
|
262
|
+
wake = r;
|
|
263
|
+
});
|
|
264
|
+
wake = undefined;
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
}),
|
|
268
|
+
};
|
|
269
|
+
const forward = {
|
|
270
|
+
send: (input) => {
|
|
271
|
+
// Copy into a plain ArrayBuffer-backed view (WebSocket.send rejects SharedArrayBuffer views).
|
|
272
|
+
ws.send(new Uint8Array(input).buffer);
|
|
273
|
+
},
|
|
274
|
+
eof: () => {
|
|
275
|
+
ws.send(JSON.stringify({ t: "eof" }));
|
|
276
|
+
},
|
|
277
|
+
output,
|
|
278
|
+
closed,
|
|
279
|
+
close: () => {
|
|
280
|
+
finish("closed");
|
|
281
|
+
ws.close();
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
ws.addEventListener("open", () => resolve(forward), { once: true });
|
|
285
|
+
ws.addEventListener("error", () => reject(new Error(`workspace forward failed: could not connect to ${url.host} (is anything listening on 127.0.0.1:${port} in the workspace?)`)), { once: true });
|
|
286
|
+
});
|
|
287
|
+
};
|
package/dist/types.d.ts
CHANGED
|
@@ -220,6 +220,31 @@ export interface Workspace {
|
|
|
220
220
|
expire(options?: {
|
|
221
221
|
readonly in?: string | null;
|
|
222
222
|
}): Promise<void>;
|
|
223
|
+
/**
|
|
224
|
+
* Open a raw TCP byte pipe to `127.0.0.1:port` INSIDE the workspace — the
|
|
225
|
+
* primitive for reaching a dev server or database the workspace runs.
|
|
226
|
+
* Protocol-agnostic: nothing inspects or records the payload. One held
|
|
227
|
+
* WebSocket per forward; rejects when nothing accepts the connection. The
|
|
228
|
+
* target host is fixed at loopback by design.
|
|
229
|
+
*/
|
|
230
|
+
forward(port: number): Promise<WorkspaceForward>;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* A live port forward — one WebSocket, held until `close()` or the remote
|
|
234
|
+
* closes. A raw duplex byte stream: write with `send`, read from `output`,
|
|
235
|
+
* signal outbound EOF with `eof` (half-close; inbound keeps flowing).
|
|
236
|
+
*/
|
|
237
|
+
export interface WorkspaceForward {
|
|
238
|
+
/** Write bytes toward the workspace port on the held socket. */
|
|
239
|
+
send(input: Uint8Array): void;
|
|
240
|
+
/** Half-close: no more outbound bytes; the remote's response keeps flowing. */
|
|
241
|
+
eof(): void;
|
|
242
|
+
/** Bytes from the workspace port, until the remote closes or `close()`. */
|
|
243
|
+
readonly output: AsyncIterable<Uint8Array>;
|
|
244
|
+
/** Resolves when the forward ends: the remote closed (`"end"`) or the socket closed. */
|
|
245
|
+
readonly closed: Promise<"end" | "closed">;
|
|
246
|
+
/** Tear the forward down. */
|
|
247
|
+
close(): void;
|
|
223
248
|
}
|
|
224
249
|
export interface RunOptions {
|
|
225
250
|
/** Cancel the run by aborting this signal. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealant/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "The fluent public SDK for Sealant — create a workspace, run a harness, replay the record.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@sealant/api-contracts": "^0.
|
|
29
|
+
"@sealant/api-contracts": "^0.14.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@effect/vitest": "4.0.0-beta.85",
|