@sealant/sdk-next 0.40.0-next.760 → 0.40.0-next.763
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/errors.d.ts +9 -0
- package/dist/errors.js +13 -0
- package/dist/facade/bounded-forward.d.ts +7 -0
- package/dist/facade/bounded-forward.js +191 -0
- package/dist/facade/forward.d.ts +3 -12
- package/dist/facade/forward.js +25 -20
- package/dist/facade/workspace.js +18 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +70 -0
- package/package.json +3 -3
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ForwardRefusalReason } from "@sealant/api-contracts";
|
|
2
|
+
import type { ForwardResetCause } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Public SDK errors. The Effect core fails on a typed `Schema.TaggedError` channel; the facade maps
|
|
4
5
|
* every tagged failure onto one of these PLAIN `Error` subclasses (a single `_tag` switch) so the
|
|
@@ -82,3 +83,11 @@ export declare class SealantNotImplementedError extends SealantError {
|
|
|
82
83
|
readonly name = "SealantNotImplementedError";
|
|
83
84
|
constructor(operation: string);
|
|
84
85
|
}
|
|
86
|
+
/** A bounded forward was reset after it opened. Its streams error with this. */
|
|
87
|
+
export declare class ForwardResetError extends SealantError {
|
|
88
|
+
readonly name: string;
|
|
89
|
+
readonly reason: ForwardResetCause;
|
|
90
|
+
/** The reason Core sent, when this SDK does not know it (`unrecognized-reset`). */
|
|
91
|
+
readonly wireReason?: string;
|
|
92
|
+
constructor(reason: ForwardResetCause, wireReason?: string);
|
|
93
|
+
}
|
package/dist/errors.js
CHANGED
|
@@ -85,3 +85,16 @@ export class SealantNotImplementedError extends SealantError {
|
|
|
85
85
|
super(`${operation} is part of the Sealant SDK surface but is not implemented in this build yet.`, { code: "not_implemented" });
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
/** A bounded forward was reset after it opened. Its streams error with this. */
|
|
89
|
+
export class ForwardResetError extends SealantError {
|
|
90
|
+
name = "ForwardResetError";
|
|
91
|
+
reason;
|
|
92
|
+
/** The reason Core sent, when this SDK does not know it (`unrecognized-reset`). */
|
|
93
|
+
wireReason;
|
|
94
|
+
constructor(reason, wireReason) {
|
|
95
|
+
super(`The forward was reset: ${wireReason ?? reason}.`, { code: "ForwardResetError" });
|
|
96
|
+
this.reason = reason;
|
|
97
|
+
if (wireReason !== undefined)
|
|
98
|
+
this.wireReason = wireReason;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BoundedWorkspaceForward } from "../types.js";
|
|
2
|
+
export declare const boundedForward: (ws: WebSocket, binding: {
|
|
3
|
+
readonly kind: "port";
|
|
4
|
+
} | {
|
|
5
|
+
readonly kind: "session";
|
|
6
|
+
readonly session: string;
|
|
7
|
+
}, onWindowOpen: () => void) => BoundedWorkspaceForward;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { CountQueuingStrategy, ReadableStream, WritableStream, } from "node:stream/web";
|
|
2
|
+
import { FORWARD_V2_MAX_FRAME_BYTES, ForwardFlow, forwardFrames, forwardResetReasonSchema, } from "@sealant/api-contracts";
|
|
3
|
+
import { Schema } from "effect";
|
|
4
|
+
import { ForwardResetError } from "../errors.js";
|
|
5
|
+
const isKnownReset = Schema.is(forwardResetReasonSchema);
|
|
6
|
+
const SOCKET_HIGH_WATER_BYTES = FORWARD_V2_MAX_FRAME_BYTES;
|
|
7
|
+
/** A WHATWG socket has no drain event, so a held `progress` checks again this often. */
|
|
8
|
+
const HELD_PROGRESS_RETRY_MS = 10;
|
|
9
|
+
export const boundedForward = (ws, binding, onWindowOpen) => {
|
|
10
|
+
const flow = new ForwardFlow();
|
|
11
|
+
const unread = [];
|
|
12
|
+
const closed = Promise.withResolvers();
|
|
13
|
+
let state = { kind: "open" };
|
|
14
|
+
let windowOpen = false;
|
|
15
|
+
let finReceived = false;
|
|
16
|
+
let reader;
|
|
17
|
+
let writer;
|
|
18
|
+
let pendingRead;
|
|
19
|
+
let creditWaiter;
|
|
20
|
+
let progressRetry;
|
|
21
|
+
const sendText = (frame) => ws.send(JSON.stringify(frame));
|
|
22
|
+
const wakeReader = () => {
|
|
23
|
+
const waiter = pendingRead;
|
|
24
|
+
pendingRead = undefined;
|
|
25
|
+
waiter?.();
|
|
26
|
+
};
|
|
27
|
+
const wakeWriter = () => {
|
|
28
|
+
const waiter = creditWaiter;
|
|
29
|
+
creditWaiter = undefined;
|
|
30
|
+
waiter?.();
|
|
31
|
+
};
|
|
32
|
+
const settle = (next, close) => {
|
|
33
|
+
if (state.kind !== "open")
|
|
34
|
+
return;
|
|
35
|
+
state = next;
|
|
36
|
+
clearTimeout(progressRetry);
|
|
37
|
+
closed.resolve(close);
|
|
38
|
+
if (next.kind === "failed") {
|
|
39
|
+
writer?.error(next.error);
|
|
40
|
+
if (next.readable === "errored")
|
|
41
|
+
reader?.error(next.error);
|
|
42
|
+
}
|
|
43
|
+
wakeReader();
|
|
44
|
+
wakeWriter();
|
|
45
|
+
};
|
|
46
|
+
const resetBy = (reason, wireReason) => settle({ kind: "failed", error: new ForwardResetError(reason, wireReason), readable: "errored" }, { kind: "reset", reason, ...(wireReason === undefined ? {} : { wireReason }) });
|
|
47
|
+
const reset = (reason) => {
|
|
48
|
+
if (state.kind !== "open")
|
|
49
|
+
return;
|
|
50
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
51
|
+
sendText({ t: "reset", reason });
|
|
52
|
+
resetBy(reason);
|
|
53
|
+
ws.close();
|
|
54
|
+
};
|
|
55
|
+
const endedByWorkspace = () => {
|
|
56
|
+
if (flow.delivered && creditWaiter === undefined) {
|
|
57
|
+
return settle({ kind: "ended" }, { kind: "end" });
|
|
58
|
+
}
|
|
59
|
+
settle({
|
|
60
|
+
kind: "failed",
|
|
61
|
+
error: new ForwardResetError("workspace-ended"),
|
|
62
|
+
readable: "drains-what-arrived",
|
|
63
|
+
}, { kind: "reset", reason: "workspace-ended" });
|
|
64
|
+
};
|
|
65
|
+
const sendProgress = () => {
|
|
66
|
+
if (state.kind !== "open" || ws.readyState !== WebSocket.OPEN || !flow.progressDue)
|
|
67
|
+
return;
|
|
68
|
+
if (ws.bufferedAmount > SOCKET_HIGH_WATER_BYTES) {
|
|
69
|
+
progressRetry ??= setTimeout(() => {
|
|
70
|
+
progressRetry = undefined;
|
|
71
|
+
sendProgress();
|
|
72
|
+
}, HELD_PROGRESS_RETRY_MS);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const progress = flow.takeProgress();
|
|
76
|
+
if (progress !== undefined)
|
|
77
|
+
sendText(progress);
|
|
78
|
+
};
|
|
79
|
+
const deliver = (controller, chunk) => {
|
|
80
|
+
controller.enqueue(chunk);
|
|
81
|
+
flow.consumed(chunk.byteLength);
|
|
82
|
+
sendProgress();
|
|
83
|
+
};
|
|
84
|
+
const onInbound = (frame) => {
|
|
85
|
+
const inbound = flow.receive(frame);
|
|
86
|
+
switch (inbound.kind) {
|
|
87
|
+
case "data":
|
|
88
|
+
if (pendingRead === undefined || reader === undefined) {
|
|
89
|
+
unread.push(inbound.data);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
deliver(reader, inbound.data);
|
|
93
|
+
return wakeReader();
|
|
94
|
+
case "progress":
|
|
95
|
+
if (!windowOpen) {
|
|
96
|
+
windowOpen = true;
|
|
97
|
+
onWindowOpen();
|
|
98
|
+
}
|
|
99
|
+
return wakeWriter();
|
|
100
|
+
case "fin":
|
|
101
|
+
finReceived = true;
|
|
102
|
+
if (unread.length === 0)
|
|
103
|
+
reader?.close();
|
|
104
|
+
wakeReader();
|
|
105
|
+
return endedByWorkspace();
|
|
106
|
+
case "reset": {
|
|
107
|
+
const known = isKnownReset(inbound.reason);
|
|
108
|
+
resetBy(known ? inbound.reason : "unrecognized-reset", known ? undefined : inbound.reason);
|
|
109
|
+
return ws.close();
|
|
110
|
+
}
|
|
111
|
+
case "violation":
|
|
112
|
+
return reset(inbound.reason);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
ws.addEventListener("message", (event) => {
|
|
116
|
+
if (state.kind !== "open")
|
|
117
|
+
return;
|
|
118
|
+
if (event.data instanceof ArrayBuffer)
|
|
119
|
+
onInbound(new Uint8Array(event.data));
|
|
120
|
+
else if (typeof event.data === "string")
|
|
121
|
+
onInbound(event.data);
|
|
122
|
+
});
|
|
123
|
+
ws.addEventListener("close", () => resetBy("connection-lost"));
|
|
124
|
+
const readable = new ReadableStream({
|
|
125
|
+
start: (controller) => {
|
|
126
|
+
reader = controller;
|
|
127
|
+
},
|
|
128
|
+
pull: (controller) => {
|
|
129
|
+
const chunk = unread.shift();
|
|
130
|
+
if (chunk === undefined) {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
pendingRead = resolve;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
deliver(controller, chunk);
|
|
136
|
+
if (finReceived && unread.length === 0)
|
|
137
|
+
controller.close();
|
|
138
|
+
},
|
|
139
|
+
cancel: () => reset("aborted"),
|
|
140
|
+
}, { highWaterMark: 0 });
|
|
141
|
+
const waitForCredit = (fits) => new Promise((resolve, reject) => {
|
|
142
|
+
const check = () => {
|
|
143
|
+
if (state.kind === "failed")
|
|
144
|
+
return reject(state.error);
|
|
145
|
+
if (state.kind === "ended")
|
|
146
|
+
return reject(new ForwardResetError("workspace-ended"));
|
|
147
|
+
if (fits())
|
|
148
|
+
return resolve();
|
|
149
|
+
creditWaiter = check;
|
|
150
|
+
};
|
|
151
|
+
check();
|
|
152
|
+
});
|
|
153
|
+
const holdReadyUntilCredit = () => waitForCredit(() => flow.canSend(1));
|
|
154
|
+
const writable = new WritableStream({
|
|
155
|
+
start: (controller) => {
|
|
156
|
+
writer = controller;
|
|
157
|
+
// The sink's own `abort` runs only after a write waiting for credit settles. Node has had
|
|
158
|
+
// `signal` since 18; its types still lack it.
|
|
159
|
+
const signal = "signal" in controller ? controller.signal : undefined;
|
|
160
|
+
if (!(signal instanceof AbortSignal))
|
|
161
|
+
return;
|
|
162
|
+
signal.addEventListener("abort", () => {
|
|
163
|
+
// The stream is already erroring: erroring it again from here breaks Node's abort.
|
|
164
|
+
writer = undefined;
|
|
165
|
+
reset("aborted");
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
write: async (chunk) => {
|
|
169
|
+
for (const frame of forwardFrames(chunk)) {
|
|
170
|
+
await waitForCredit(() => flow.canSend(frame.byteLength));
|
|
171
|
+
flow.sent(frame.byteLength);
|
|
172
|
+
// A copy: a WHATWG socket refuses a view of a SharedArrayBuffer.
|
|
173
|
+
ws.send(new Uint8Array(frame));
|
|
174
|
+
}
|
|
175
|
+
await holdReadyUntilCredit();
|
|
176
|
+
},
|
|
177
|
+
close: () => {
|
|
178
|
+
if (state.kind === "open")
|
|
179
|
+
sendText(flow.fin());
|
|
180
|
+
},
|
|
181
|
+
abort: () => reset("aborted"),
|
|
182
|
+
}, new CountQueuingStrategy({ highWaterMark: 1 }));
|
|
183
|
+
sendText(flow.open());
|
|
184
|
+
return {
|
|
185
|
+
readable,
|
|
186
|
+
writable,
|
|
187
|
+
binding,
|
|
188
|
+
closed: closed.promise,
|
|
189
|
+
close: () => reset("aborted"),
|
|
190
|
+
};
|
|
191
|
+
};
|
package/dist/facade/forward.d.ts
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { WorkspaceForward, WorkspaceForwardOptions } from "../types.js";
|
|
1
|
+
import type { BoundedForwardTarget, BoundedWorkspaceForward, BoundedWorkspaceForwardOptions, WorkspaceForward, WorkspaceForwardOptions } from "../types.js";
|
|
3
2
|
export interface ForwardEndpoint {
|
|
4
3
|
readonly baseUrl: string;
|
|
5
4
|
readonly apiKey: string | undefined;
|
|
6
5
|
readonly ownerUserId: string;
|
|
7
6
|
}
|
|
8
7
|
export declare const openForward: (endpoint: ForwardEndpoint, workspaceId: string, port: number, options?: WorkspaceForwardOptions | undefined) => Promise<WorkspaceForward>;
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
readonly signal?: AbortSignal;
|
|
12
|
-
readonly deadlineMs?: number;
|
|
13
|
-
}
|
|
14
|
-
export interface OpenedForwardV2 {
|
|
15
|
-
readonly binding: ForwardBinding;
|
|
16
|
-
readonly forward: WorkspaceForward;
|
|
17
|
-
}
|
|
18
|
-
export declare const openForwardV2: (endpoint: ForwardEndpoint, workspaceId: string, port: number, options?: ForwardV2Options | undefined) => Promise<OpenedForwardV2>;
|
|
8
|
+
export type ForwardV2Options = BoundedForwardTarget & Pick<BoundedWorkspaceForwardOptions, "signal" | "deadlineMs">;
|
|
9
|
+
export declare const openForwardV2: (endpoint: ForwardEndpoint, workspaceId: string, port: number, options?: ForwardV2Options | undefined) => Promise<BoundedWorkspaceForward>;
|
package/dist/facade/forward.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FORWARD_REFUSAL_STATUS, FORWARD_V2_SUBPROTOCOL, forwardOpenFrameSchema, forwardRefusalReasonSchema, } from "@sealant/api-contracts";
|
|
2
2
|
import { Option, Schema } from "effect";
|
|
3
3
|
import { ForwardDialError } from "../errors.js";
|
|
4
|
+
import { boundedForward } from "./bounded-forward.js";
|
|
4
5
|
const forwardUrl = (endpoint, path, query) => {
|
|
5
6
|
const url = new URL(path, endpoint.baseUrl);
|
|
6
7
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
@@ -16,7 +17,7 @@ const forwardUrl = (endpoint, path, query) => {
|
|
|
16
17
|
}
|
|
17
18
|
return url;
|
|
18
19
|
};
|
|
19
|
-
const bridgeForward = (ws
|
|
20
|
+
const bridgeForward = (ws) => {
|
|
20
21
|
const pending = [];
|
|
21
22
|
let wake;
|
|
22
23
|
let finished = false;
|
|
@@ -30,7 +31,7 @@ const bridgeForward = (ws, maxFrameBytes = Number.POSITIVE_INFINITY) => {
|
|
|
30
31
|
wake?.();
|
|
31
32
|
};
|
|
32
33
|
ws.addEventListener("close", () => finish("closed"));
|
|
33
|
-
|
|
34
|
+
ws.addEventListener("message", (event) => {
|
|
34
35
|
if (typeof event.data === "string") {
|
|
35
36
|
try {
|
|
36
37
|
const frame = JSON.parse(event.data);
|
|
@@ -43,7 +44,7 @@ const bridgeForward = (ws, maxFrameBytes = Number.POSITIVE_INFINITY) => {
|
|
|
43
44
|
}
|
|
44
45
|
pending.push(new Uint8Array(event.data));
|
|
45
46
|
wake?.();
|
|
46
|
-
};
|
|
47
|
+
});
|
|
47
48
|
const output = {
|
|
48
49
|
[Symbol.asyncIterator]: () => ({
|
|
49
50
|
next: async () => {
|
|
@@ -63,14 +64,10 @@ const bridgeForward = (ws, maxFrameBytes = Number.POSITIVE_INFINITY) => {
|
|
|
63
64
|
},
|
|
64
65
|
}),
|
|
65
66
|
};
|
|
66
|
-
|
|
67
|
+
return {
|
|
67
68
|
send: (input) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Copy into a plain ArrayBuffer-backed view (WebSocket.send rejects SharedArrayBuffer views).
|
|
71
|
-
ws.send(new Uint8Array(input.subarray(offset, offset + maxFrameBytes)).buffer);
|
|
72
|
-
offset += maxFrameBytes;
|
|
73
|
-
} while (offset < input.byteLength);
|
|
69
|
+
// Copy into a plain ArrayBuffer-backed view (WebSocket.send rejects SharedArrayBuffer views).
|
|
70
|
+
ws.send(new Uint8Array(input).buffer);
|
|
74
71
|
},
|
|
75
72
|
eof: () => {
|
|
76
73
|
ws.send(JSON.stringify({ t: "eof" }));
|
|
@@ -82,7 +79,6 @@ const bridgeForward = (ws, maxFrameBytes = Number.POSITIVE_INFINITY) => {
|
|
|
82
79
|
ws.close();
|
|
83
80
|
},
|
|
84
81
|
};
|
|
85
|
-
return { forward, receive };
|
|
86
82
|
};
|
|
87
83
|
export const openForward = (endpoint, workspaceId, port, options) => {
|
|
88
84
|
const url = forwardUrl(endpoint, `/v1/workspaces/${workspaceId}/forward`, {
|
|
@@ -93,8 +89,7 @@ export const openForward = (endpoint, workspaceId, port, options) => {
|
|
|
93
89
|
return new Promise((resolve, reject) => {
|
|
94
90
|
const ws = new WebSocket(url);
|
|
95
91
|
ws.binaryType = "arraybuffer";
|
|
96
|
-
const
|
|
97
|
-
ws.addEventListener("message", receive);
|
|
92
|
+
const forward = bridgeForward(ws);
|
|
98
93
|
ws.addEventListener("open", () => resolve(forward), { once: true });
|
|
99
94
|
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 });
|
|
100
95
|
});
|
|
@@ -136,11 +131,13 @@ const failedHandshakeError = async (url, origin, signal) => {
|
|
|
136
131
|
};
|
|
137
132
|
export const openForwardV2 = (endpoint, workspaceId, port, options) => {
|
|
138
133
|
const host = options?.host ?? "127.0.0.1";
|
|
134
|
+
const session = options?.session;
|
|
139
135
|
const signal = options?.signal;
|
|
140
136
|
const deadlineMs = options?.deadlineMs ?? FORWARD_V2_OPEN_DEADLINE_MS;
|
|
141
137
|
const url = forwardUrl(endpoint, `/v1/workspaces/${workspaceId}/forward/v2`, {
|
|
142
138
|
port: String(port),
|
|
143
139
|
host,
|
|
140
|
+
session,
|
|
144
141
|
});
|
|
145
142
|
const origin = new URL(endpoint.baseUrl).origin;
|
|
146
143
|
return new Promise((resolve, reject) => {
|
|
@@ -148,7 +145,6 @@ export const openForwardV2 = (endpoint, workspaceId, port, options) => {
|
|
|
148
145
|
return reject(signal.reason);
|
|
149
146
|
const ws = new WebSocket(url, FORWARD_V2_SUBPROTOCOL);
|
|
150
147
|
ws.binaryType = "arraybuffer";
|
|
151
|
-
const { forward, receive } = bridgeForward(ws, FORWARD_V2_MAX_FRAME_BYTES);
|
|
152
148
|
const probing = new AbortController();
|
|
153
149
|
let phase = "handshake";
|
|
154
150
|
const settle = (next) => {
|
|
@@ -178,8 +174,6 @@ export const openForwardV2 = (endpoint, workspaceId, port, options) => {
|
|
|
178
174
|
}
|
|
179
175
|
});
|
|
180
176
|
ws.addEventListener("message", (event) => {
|
|
181
|
-
if (phase === "open")
|
|
182
|
-
return receive(event);
|
|
183
177
|
if (phase !== "awaiting-answer")
|
|
184
178
|
return;
|
|
185
179
|
const frame = typeof event.data === "string" ? decodeOpenFrame(event.data) : Option.none();
|
|
@@ -188,11 +182,22 @@ export const openForwardV2 = (endpoint, workspaceId, port, options) => {
|
|
|
188
182
|
}
|
|
189
183
|
if (frame.value.t === "refused")
|
|
190
184
|
return fail(refusalError(frame.value));
|
|
191
|
-
|
|
192
|
-
|
|
185
|
+
const { binding } = frame.value;
|
|
186
|
+
if (session !== undefined && (binding.kind !== "session" || binding.session !== session)) {
|
|
187
|
+
return fail(new ForwardDialError({
|
|
188
|
+
reason: "binding-unsupported",
|
|
189
|
+
status: FORWARD_REFUSAL_STATUS["binding-unsupported"],
|
|
190
|
+
message: `${origin} opened the forward without binding it to session ${session}.`,
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
phase = "awaiting-window";
|
|
194
|
+
const forward = boundedForward(ws, binding, () => {
|
|
195
|
+
settle("open");
|
|
196
|
+
resolve(forward);
|
|
197
|
+
});
|
|
193
198
|
});
|
|
194
199
|
ws.addEventListener("close", () => {
|
|
195
|
-
if (phase === "awaiting-answer") {
|
|
200
|
+
if (phase === "awaiting-answer" || phase === "awaiting-window") {
|
|
196
201
|
return fail(new ForwardDialError({
|
|
197
202
|
reason: "transport-unavailable",
|
|
198
203
|
message: `The forward's connection to ${origin} closed before the control plane answered.`,
|
package/dist/facade/workspace.js
CHANGED
|
@@ -7,7 +7,7 @@ import { elapsed } from "../internal/clock.js";
|
|
|
7
7
|
import { mapAccountRef } from "../internal/credentials.js";
|
|
8
8
|
import { parseTtlSeconds } from "../internal/duration.js";
|
|
9
9
|
import { requireProcessUser } from "../internal/process-user.js";
|
|
10
|
-
import { openForward } from "./forward.js";
|
|
10
|
+
import { openForward, openForwardV2 } from "./forward.js";
|
|
11
11
|
import { makeInteractiveSession } from "./session.js";
|
|
12
12
|
/** The wire runtime as the SDK reports it. */
|
|
13
13
|
export const toRuntimeInfo = (runtime) => ({
|
|
@@ -295,6 +295,22 @@ const olderControlPlaneRefusal = (argv, error) => {
|
|
|
295
295
|
});
|
|
296
296
|
};
|
|
297
297
|
export const makeWorkspace = (ctx, init) => {
|
|
298
|
+
const endpoint = {
|
|
299
|
+
baseUrl: ctx.config.baseUrl,
|
|
300
|
+
apiKey: ctx.config.apiKey,
|
|
301
|
+
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
302
|
+
};
|
|
303
|
+
function forward(port, options) {
|
|
304
|
+
if (options?.flow === "bounded") {
|
|
305
|
+
return openForwardV2(endpoint, init.id, port, options);
|
|
306
|
+
}
|
|
307
|
+
if (options !== undefined && "session" in options) {
|
|
308
|
+
return Promise.reject(new SealantError('A forward bound to a session needs flow: "bounded".', {
|
|
309
|
+
code: "invalid_options",
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
return openForward(endpoint, init.id, port, options);
|
|
313
|
+
}
|
|
298
314
|
const openSession = async (argv, options) => {
|
|
299
315
|
// The contract's own rule, checked before anything is asked, with the same value-free reason.
|
|
300
316
|
const refused = sessionArgvIssue(argv);
|
|
@@ -801,11 +817,7 @@ export const makeWorkspace = (ctx, init) => {
|
|
|
801
817
|
sshAsRoot: async () => {
|
|
802
818
|
await ctx.runtime.run(clearWorkspaceSshUserOp(init.id, { ownerUserId: ctx.config.hostLocal.ownerUserId }));
|
|
803
819
|
},
|
|
804
|
-
forward
|
|
805
|
-
baseUrl: ctx.config.baseUrl,
|
|
806
|
-
apiKey: ctx.config.apiKey,
|
|
807
|
-
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
808
|
-
}, init.id, port, options),
|
|
820
|
+
forward,
|
|
809
821
|
};
|
|
810
822
|
return workspace;
|
|
811
823
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export { Sealant } from "./client.js";
|
|
18
18
|
export { claudeCode, codex, customHarness, opencode, pi } from "./harness.js";
|
|
19
|
-
export { ForwardDialError, type ForwardDialFailure, type ForwardDialReason, SealantApiError, SealantError, SealantNotImplementedError, SealantRuntimeError, } from "./errors.js";
|
|
19
|
+
export { ForwardDialError, type ForwardDialFailure, type ForwardDialReason, ForwardResetError, SealantApiError, SealantError, SealantNotImplementedError, SealantRuntimeError, } from "./errors.js";
|
|
20
20
|
export type * from "./types.js";
|
|
21
21
|
export { findWorkspaceEnvReservedRule, findWorkspaceSecretEnvReservedRule, formatWorkspaceEnvIssue, parseWorkspaceEnv, parseWorkspaceSecretEnv, WORKSPACE_ENV_MAX_ENTRIES, WORKSPACE_ENV_MAX_NAME_LENGTH, WORKSPACE_ENV_MAX_TOTAL_BYTES, WORKSPACE_ENV_MAX_VALUE_BYTES, WORKSPACE_ENV_NAME_PATTERN, WORKSPACE_ENV_SECRET_MARKERS, } from "@sealant/api-contracts/workspace-environment";
|
|
22
22
|
export type { WorkspaceEnvIssue, WorkspaceEnvParseResult, WorkspaceEnvReservedRule, } from "@sealant/api-contracts/workspace-environment";
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export { Sealant } from "./client.js";
|
|
18
18
|
export { claudeCode, codex, customHarness, opencode, pi } from "./harness.js";
|
|
19
|
-
export { ForwardDialError, SealantApiError, SealantError, SealantNotImplementedError, SealantRuntimeError, } from "./errors.js";
|
|
19
|
+
export { ForwardDialError, ForwardResetError, SealantApiError, SealantError, SealantNotImplementedError, SealantRuntimeError, } from "./errors.js";
|
|
20
20
|
// The workspace environment policy (`CreateOptions.env` validation) is public API: downstream
|
|
21
21
|
// products validate at their own boundaries with the exact rules the platform enforces.
|
|
22
22
|
export { findWorkspaceEnvReservedRule, findWorkspaceSecretEnvReservedRule, formatWorkspaceEnvIssue, parseWorkspaceEnv, parseWorkspaceSecretEnv, WORKSPACE_ENV_MAX_ENTRIES, WORKSPACE_ENV_MAX_NAME_LENGTH, WORKSPACE_ENV_MAX_TOTAL_BYTES, WORKSPACE_ENV_MAX_VALUE_BYTES, WORKSPACE_ENV_NAME_PATTERN, WORKSPACE_ENV_SECRET_MARKERS, } from "@sealant/api-contracts/workspace-environment";
|
package/dist/types.d.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* compile against a stable contract from day one (unimplemented paths reject with
|
|
15
15
|
* `SealantNotImplementedError` at runtime, see `./errors.js`).
|
|
16
16
|
*/
|
|
17
|
+
import type { ReadableStream as NodeReadableStream, WritableStream as NodeWritableStream } from "node:stream/web";
|
|
18
|
+
import type { ForwardBinding, ForwardResetReason } from "@sealant/api-contracts";
|
|
17
19
|
/**
|
|
18
20
|
* Public client configuration. Intentionally minimal: a base URL and an API key. Host-local
|
|
19
21
|
* concerns required by the current slice (owner identity, registry, direct database access) live in
|
|
@@ -1160,7 +1162,13 @@ export interface Workspace {
|
|
|
1160
1162
|
* workspace-private set: the container's loopback (default), or `docker` —
|
|
1161
1163
|
* the workspace-scoped Docker sidecar's alias, where `docker compose`
|
|
1162
1164
|
* publishes its ports.
|
|
1165
|
+
*
|
|
1166
|
+
* With `flow: "bounded"`, a TCP forward whose two directions are bounded: the workspace's
|
|
1167
|
+
* producer is held back while `readable` is not read, and `writable` holds back its writer while
|
|
1168
|
+
* the workspace does not take the bytes. It resolves once Core and the workspace runtime have
|
|
1169
|
+
* opened it, and rejects with a {@link ForwardDialError} that says why when they did not.
|
|
1163
1170
|
*/
|
|
1171
|
+
forward(port: number, options: BoundedWorkspaceForwardOptions): Promise<BoundedWorkspaceForward>;
|
|
1164
1172
|
forward(port: number, options?: WorkspaceForwardOptions): Promise<WorkspaceForward>;
|
|
1165
1173
|
}
|
|
1166
1174
|
/**
|
|
@@ -1442,6 +1450,10 @@ export interface WorkspaceCredentialsHomeOptions {
|
|
|
1442
1450
|
}
|
|
1443
1451
|
/** Options for {@link Workspace.forward}. */
|
|
1444
1452
|
export interface WorkspaceForwardOptions {
|
|
1453
|
+
/** Only a bounded forward has a `flow`. */
|
|
1454
|
+
readonly flow?: never;
|
|
1455
|
+
/** Only a bounded forward can be bound to a session. */
|
|
1456
|
+
readonly session?: never;
|
|
1445
1457
|
/** Target inside the workspace: its loopback (default) or the Docker sidecar. */
|
|
1446
1458
|
readonly host?: "127.0.0.1" | "localhost" | "docker";
|
|
1447
1459
|
/**
|
|
@@ -1469,6 +1481,64 @@ export interface WorkspaceForward {
|
|
|
1469
1481
|
/** Tear the forward down. */
|
|
1470
1482
|
close(): void;
|
|
1471
1483
|
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Where a bounded forward goes: the port on the workspace's loopback (default) or on the Docker
|
|
1486
|
+
* sidecar, whatever holds it; or, with `session`, the port on the loopback only when a process of
|
|
1487
|
+
* that session holds it. A bound forward resets with `owner-exited` once the session's leader exits.
|
|
1488
|
+
*/
|
|
1489
|
+
export type BoundedForwardTarget = {
|
|
1490
|
+
readonly host?: "127.0.0.1" | "localhost" | "docker";
|
|
1491
|
+
readonly session?: never;
|
|
1492
|
+
} | {
|
|
1493
|
+
readonly host?: "127.0.0.1" | "localhost";
|
|
1494
|
+
readonly session: string;
|
|
1495
|
+
};
|
|
1496
|
+
/** Options for a bounded {@link Workspace.forward}. TCP only. */
|
|
1497
|
+
export type BoundedWorkspaceForwardOptions = BoundedForwardTarget & {
|
|
1498
|
+
readonly flow: "bounded";
|
|
1499
|
+
/** Gives up opening the forward; an open forward ignores it. */
|
|
1500
|
+
readonly signal?: AbortSignal;
|
|
1501
|
+
/** How long opening may take before it rejects, in milliseconds. 30 000 by default. */
|
|
1502
|
+
readonly deadlineMs?: number;
|
|
1503
|
+
};
|
|
1504
|
+
/**
|
|
1505
|
+
* Why a bounded forward was reset: a reason either side sent, `connection-lost` when the socket
|
|
1506
|
+
* closed with neither a `fin` nor a reset, `unrecognized-reset` when Core sent a reason newer than
|
|
1507
|
+
* this SDK (kept in `wireReason`), or `workspace-ended` when the workspace ended the forward before
|
|
1508
|
+
* it took everything written to it. A write after the workspace ended fails with `workspace-ended`
|
|
1509
|
+
* too.
|
|
1510
|
+
*/
|
|
1511
|
+
export type ForwardResetCause = ForwardResetReason | "connection-lost" | "unrecognized-reset" | "workspace-ended";
|
|
1512
|
+
/**
|
|
1513
|
+
* How a bounded forward ended: the workspace closed its side after it took everything written to
|
|
1514
|
+
* it (`end`), or the forward was reset.
|
|
1515
|
+
*/
|
|
1516
|
+
export type BoundedWorkspaceForwardClose = {
|
|
1517
|
+
readonly kind: "end";
|
|
1518
|
+
} | {
|
|
1519
|
+
readonly kind: "reset";
|
|
1520
|
+
readonly reason: ForwardResetCause;
|
|
1521
|
+
readonly wireReason?: string;
|
|
1522
|
+
};
|
|
1523
|
+
/**
|
|
1524
|
+
* A bounded forward. Reading `readable` is what lets the workspace send more; `writer.ready` waits
|
|
1525
|
+
* while the workspace can take no more; `writable.close()` half-closes toward the workspace. Wrap
|
|
1526
|
+
* it as a Node stream with `Duplex.fromWeb(forward)`. The streams are typed as `node:stream/web`'s,
|
|
1527
|
+
* which `Duplex.fromWeb` accepts also where the DOM's stream types are loaded.
|
|
1528
|
+
*
|
|
1529
|
+
* A reset fails both streams with a `ForwardResetError`, at once: data the workspace sent that was
|
|
1530
|
+
* not read yet is dropped. When the workspace ends first, `readable` still yields all it sent, and
|
|
1531
|
+
* the writer fails with `workspace-ended` only if something written did not reach the workspace.
|
|
1532
|
+
*/
|
|
1533
|
+
export interface BoundedWorkspaceForward {
|
|
1534
|
+
readonly readable: NodeReadableStream<Uint8Array>;
|
|
1535
|
+
readonly writable: NodeWritableStream<Uint8Array>;
|
|
1536
|
+
/** What the forward is bound to. */
|
|
1537
|
+
readonly binding: ForwardBinding;
|
|
1538
|
+
readonly closed: Promise<BoundedWorkspaceForwardClose>;
|
|
1539
|
+
/** Resets both directions. */
|
|
1540
|
+
close(): void;
|
|
1541
|
+
}
|
|
1472
1542
|
export interface RunOptions {
|
|
1473
1543
|
/** Cancel the run by aborting this signal. */
|
|
1474
1544
|
readonly signal?: AbortSignal;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealant/sdk-next",
|
|
3
|
-
"version": "0.40.0-next.
|
|
3
|
+
"version": "0.40.0-next.763",
|
|
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,12 +26,12 @@
|
|
|
26
26
|
"node": ">=20"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@sealant/api-contracts": "npm:@sealant/api-contracts-next@0.40.0-next.
|
|
29
|
+
"@sealant/api-contracts": "npm:@sealant/api-contracts-next@0.40.0-next.763"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"effect": "^4.0.5"
|
|
33
33
|
},
|
|
34
|
-
"gitHead": "
|
|
34
|
+
"gitHead": "c741e10743b2c553b526f93565c256fe8fa4bd8b",
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
|
37
37
|
}
|