@sealant/sdk-next 0.40.0-next.756 → 0.40.0-next.758
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 +30 -1
- package/dist/errors.js +18 -0
- package/dist/facade/forward.d.ts +18 -0
- package/dist/facade/forward.js +207 -0
- package/dist/facade/workspace.js +17 -106
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +8 -5
- package/package.json +3 -3
package/dist/errors.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ForwardRefusalReason } from "@sealant/api-contracts";
|
|
1
2
|
/**
|
|
2
3
|
* Public SDK errors. The Effect core fails on a typed `Schema.TaggedError` channel; the facade maps
|
|
3
4
|
* every tagged failure onto one of these PLAIN `Error` subclasses (a single `_tag` switch) so the
|
|
@@ -23,7 +24,7 @@ export declare class SealantRuntimeError extends SealantError {
|
|
|
23
24
|
}
|
|
24
25
|
/** A control-plane API request failed (HTTP-level or typed API error). */
|
|
25
26
|
export declare class SealantApiError extends SealantError {
|
|
26
|
-
readonly name
|
|
27
|
+
readonly name: string;
|
|
27
28
|
/** HTTP status, when the failure came from a response. */
|
|
28
29
|
readonly status?: number;
|
|
29
30
|
/**
|
|
@@ -48,6 +49,34 @@ export declare class SealantApiError extends SealantError {
|
|
|
48
49
|
readonly cause?: unknown;
|
|
49
50
|
});
|
|
50
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Why a workspace forward did not open. Core names every refusal it sends. The SDK adds
|
|
54
|
+
* `unrecognized-refusal` (Core sent a reason newer than this SDK; it is kept in `wireReason`),
|
|
55
|
+
* `protocol-unsupported` (the control plane predates typed refusals, or answered with something
|
|
56
|
+
* unreadable) and `transport-unavailable` (no answer could be read at all).
|
|
57
|
+
*/
|
|
58
|
+
export type ForwardDialReason = ForwardRefusalReason | "unrecognized-refusal" | "protocol-unsupported" | "transport-unavailable";
|
|
59
|
+
export type ForwardDialFailure = {
|
|
60
|
+
readonly reason: ForwardRefusalReason;
|
|
61
|
+
readonly status: number;
|
|
62
|
+
readonly message: string;
|
|
63
|
+
} | {
|
|
64
|
+
readonly reason: "unrecognized-refusal";
|
|
65
|
+
readonly wireReason: string;
|
|
66
|
+
readonly status: number;
|
|
67
|
+
readonly message: string;
|
|
68
|
+
} | {
|
|
69
|
+
readonly reason: "protocol-unsupported" | "transport-unavailable";
|
|
70
|
+
readonly message: string;
|
|
71
|
+
};
|
|
72
|
+
/** A workspace forward was refused before it opened. Branch on `reason`. */
|
|
73
|
+
export declare class ForwardDialError extends SealantApiError {
|
|
74
|
+
readonly name: string;
|
|
75
|
+
readonly reason: ForwardDialReason;
|
|
76
|
+
/** The reason Core sent, when this SDK does not know it (`unrecognized-refusal`). */
|
|
77
|
+
readonly wireReason?: string;
|
|
78
|
+
constructor(failure: ForwardDialFailure);
|
|
79
|
+
}
|
|
51
80
|
/** The requested operation is part of the public surface but not yet implemented in this slice. */
|
|
52
81
|
export declare class SealantNotImplementedError extends SealantError {
|
|
53
82
|
readonly name = "SealantNotImplementedError";
|
package/dist/errors.js
CHANGED
|
@@ -60,6 +60,24 @@ export class SealantApiError extends SealantError {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
/** A workspace forward was refused before it opened. Branch on `reason`. */
|
|
64
|
+
export class ForwardDialError extends SealantApiError {
|
|
65
|
+
name = "ForwardDialError";
|
|
66
|
+
reason;
|
|
67
|
+
/** The reason Core sent, when this SDK does not know it (`unrecognized-refusal`). */
|
|
68
|
+
wireReason;
|
|
69
|
+
constructor(failure) {
|
|
70
|
+
super(failure.message, {
|
|
71
|
+
code: "ForwardDialError",
|
|
72
|
+
reason: failure.reason,
|
|
73
|
+
...("status" in failure ? { status: failure.status } : {}),
|
|
74
|
+
});
|
|
75
|
+
this.reason = failure.reason;
|
|
76
|
+
if ("wireReason" in failure) {
|
|
77
|
+
this.wireReason = failure.wireReason;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
63
81
|
/** The requested operation is part of the public surface but not yet implemented in this slice. */
|
|
64
82
|
export class SealantNotImplementedError extends SealantError {
|
|
65
83
|
name = "SealantNotImplementedError";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ForwardBinding } from "@sealant/api-contracts";
|
|
2
|
+
import type { WorkspaceForward, WorkspaceForwardOptions } from "../types.js";
|
|
3
|
+
export interface ForwardEndpoint {
|
|
4
|
+
readonly baseUrl: string;
|
|
5
|
+
readonly apiKey: string | undefined;
|
|
6
|
+
readonly ownerUserId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const openForward: (endpoint: ForwardEndpoint, workspaceId: string, port: number, options?: WorkspaceForwardOptions | undefined) => Promise<WorkspaceForward>;
|
|
9
|
+
export interface ForwardV2Options {
|
|
10
|
+
readonly host?: "127.0.0.1" | "localhost" | "docker";
|
|
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>;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { FORWARD_V2_MAX_FRAME_BYTES, FORWARD_V2_SUBPROTOCOL, forwardOpenFrameSchema, forwardRefusalReasonSchema, } from "@sealant/api-contracts";
|
|
2
|
+
import { Option, Schema } from "effect";
|
|
3
|
+
import { ForwardDialError } from "../errors.js";
|
|
4
|
+
const forwardUrl = (endpoint, path, query) => {
|
|
5
|
+
const url = new URL(path, endpoint.baseUrl);
|
|
6
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
7
|
+
for (const [name, value] of Object.entries(query)) {
|
|
8
|
+
if (value !== undefined)
|
|
9
|
+
url.searchParams.set(name, value);
|
|
10
|
+
}
|
|
11
|
+
// The owner assertion always rides the URL (a service principal needs it alongside its
|
|
12
|
+
// key, and WebSocket cannot carry headers — same contract as the session attach).
|
|
13
|
+
url.searchParams.set("ownerUserId", endpoint.ownerUserId);
|
|
14
|
+
if (endpoint.apiKey !== undefined) {
|
|
15
|
+
url.searchParams.set("token", endpoint.apiKey);
|
|
16
|
+
}
|
|
17
|
+
return url;
|
|
18
|
+
};
|
|
19
|
+
const bridgeForward = (ws, maxFrameBytes = Number.POSITIVE_INFINITY) => {
|
|
20
|
+
const pending = [];
|
|
21
|
+
let wake;
|
|
22
|
+
let finished = false;
|
|
23
|
+
const closedResolver = Promise.withResolvers();
|
|
24
|
+
const finish = (reason) => {
|
|
25
|
+
if (finished) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
finished = true;
|
|
29
|
+
closedResolver.resolve(reason);
|
|
30
|
+
wake?.();
|
|
31
|
+
};
|
|
32
|
+
ws.addEventListener("close", () => finish("closed"));
|
|
33
|
+
const receive = (event) => {
|
|
34
|
+
if (typeof event.data === "string") {
|
|
35
|
+
try {
|
|
36
|
+
const frame = JSON.parse(event.data);
|
|
37
|
+
if (frame.t === "end") {
|
|
38
|
+
finish("end");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
pending.push(new Uint8Array(event.data));
|
|
45
|
+
wake?.();
|
|
46
|
+
};
|
|
47
|
+
const output = {
|
|
48
|
+
[Symbol.asyncIterator]: () => ({
|
|
49
|
+
next: async () => {
|
|
50
|
+
for (;;) {
|
|
51
|
+
const chunk = pending.shift();
|
|
52
|
+
if (chunk !== undefined) {
|
|
53
|
+
return { done: false, value: chunk };
|
|
54
|
+
}
|
|
55
|
+
if (finished) {
|
|
56
|
+
return { done: true, value: undefined };
|
|
57
|
+
}
|
|
58
|
+
await new Promise((r) => {
|
|
59
|
+
wake = r;
|
|
60
|
+
});
|
|
61
|
+
wake = undefined;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
const forward = {
|
|
67
|
+
send: (input) => {
|
|
68
|
+
let offset = 0;
|
|
69
|
+
do {
|
|
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);
|
|
74
|
+
},
|
|
75
|
+
eof: () => {
|
|
76
|
+
ws.send(JSON.stringify({ t: "eof" }));
|
|
77
|
+
},
|
|
78
|
+
output,
|
|
79
|
+
closed: closedResolver.promise,
|
|
80
|
+
close: () => {
|
|
81
|
+
finish("closed");
|
|
82
|
+
ws.close();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
return { forward, receive };
|
|
86
|
+
};
|
|
87
|
+
export const openForward = (endpoint, workspaceId, port, options) => {
|
|
88
|
+
const url = forwardUrl(endpoint, `/v1/workspaces/${workspaceId}/forward`, {
|
|
89
|
+
port: String(port),
|
|
90
|
+
host: options?.host,
|
|
91
|
+
protocol: options?.protocol === "udp" ? "udp" : undefined,
|
|
92
|
+
});
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const ws = new WebSocket(url);
|
|
95
|
+
ws.binaryType = "arraybuffer";
|
|
96
|
+
const { forward, receive } = bridgeForward(ws);
|
|
97
|
+
ws.addEventListener("message", receive);
|
|
98
|
+
ws.addEventListener("open", () => resolve(forward), { once: true });
|
|
99
|
+
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
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const FORWARD_V2_OPEN_DEADLINE_MS = 30_000;
|
|
103
|
+
const decodeOpenFrame = Schema.decodeUnknownOption(Schema.fromJsonString(forwardOpenFrameSchema));
|
|
104
|
+
const isKnownRefusal = Schema.is(forwardRefusalReasonSchema);
|
|
105
|
+
const refusalError = (frame) => new ForwardDialError(isKnownRefusal(frame.reason)
|
|
106
|
+
? { reason: frame.reason, status: frame.status, message: frame.message }
|
|
107
|
+
: {
|
|
108
|
+
reason: "unrecognized-refusal",
|
|
109
|
+
wireReason: frame.reason,
|
|
110
|
+
status: frame.status,
|
|
111
|
+
message: frame.message,
|
|
112
|
+
});
|
|
113
|
+
/** A gated Core before v2 answers the route 401, an open one 404. Core with v2 answers 426. */
|
|
114
|
+
const answeredLikeCoreBeforeV2 = (status) => status === 401 || status === 404;
|
|
115
|
+
const failedHandshakeError = async (url, origin, signal) => {
|
|
116
|
+
const probe = new URL(url.pathname, url);
|
|
117
|
+
probe.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
118
|
+
const unavailable = (detail) => new ForwardDialError({
|
|
119
|
+
reason: "transport-unavailable",
|
|
120
|
+
message: `The forward could not open a WebSocket to ${origin}; ${detail}.`,
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetch(probe, { signal });
|
|
124
|
+
await response.body?.cancel();
|
|
125
|
+
if (answeredLikeCoreBeforeV2(response.status)) {
|
|
126
|
+
return new ForwardDialError({
|
|
127
|
+
reason: "protocol-unsupported",
|
|
128
|
+
message: `${origin} does not serve /forward/v2; it predates typed forward refusals.`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return unavailable(`a plain request to the route got ${response.status}`);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return unavailable("a plain request to the route got no answer");
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
export const openForwardV2 = (endpoint, workspaceId, port, options) => {
|
|
138
|
+
const host = options?.host ?? "127.0.0.1";
|
|
139
|
+
const signal = options?.signal;
|
|
140
|
+
const deadlineMs = options?.deadlineMs ?? FORWARD_V2_OPEN_DEADLINE_MS;
|
|
141
|
+
const url = forwardUrl(endpoint, `/v1/workspaces/${workspaceId}/forward/v2`, {
|
|
142
|
+
port: String(port),
|
|
143
|
+
host,
|
|
144
|
+
});
|
|
145
|
+
const origin = new URL(endpoint.baseUrl).origin;
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
if (signal?.aborted === true)
|
|
148
|
+
return reject(signal.reason);
|
|
149
|
+
const ws = new WebSocket(url, FORWARD_V2_SUBPROTOCOL);
|
|
150
|
+
ws.binaryType = "arraybuffer";
|
|
151
|
+
const { forward, receive } = bridgeForward(ws, FORWARD_V2_MAX_FRAME_BYTES);
|
|
152
|
+
const probing = new AbortController();
|
|
153
|
+
let phase = "handshake";
|
|
154
|
+
const settle = (next) => {
|
|
155
|
+
phase = next;
|
|
156
|
+
clearTimeout(deadline);
|
|
157
|
+
signal?.removeEventListener("abort", onAbort);
|
|
158
|
+
probing.abort();
|
|
159
|
+
};
|
|
160
|
+
const fail = (error) => {
|
|
161
|
+
if (phase === "open" || phase === "failed")
|
|
162
|
+
return;
|
|
163
|
+
settle("failed");
|
|
164
|
+
ws.close();
|
|
165
|
+
reject(error);
|
|
166
|
+
};
|
|
167
|
+
const unreadable = (message) => fail(new ForwardDialError({ reason: "protocol-unsupported", message }));
|
|
168
|
+
const onAbort = () => fail(signal?.reason);
|
|
169
|
+
const deadline = setTimeout(() => fail(new ForwardDialError({
|
|
170
|
+
reason: "transport-unavailable",
|
|
171
|
+
message: `${origin} did not open the forward within ${deadlineMs} ms.`,
|
|
172
|
+
})), deadlineMs);
|
|
173
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
174
|
+
ws.addEventListener("open", () => {
|
|
175
|
+
phase = "awaiting-answer";
|
|
176
|
+
if (ws.protocol !== FORWARD_V2_SUBPROTOCOL) {
|
|
177
|
+
unreadable(`${origin} accepted the forward without the ${FORWARD_V2_SUBPROTOCOL} protocol.`);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
ws.addEventListener("message", (event) => {
|
|
181
|
+
if (phase === "open")
|
|
182
|
+
return receive(event);
|
|
183
|
+
if (phase !== "awaiting-answer")
|
|
184
|
+
return;
|
|
185
|
+
const frame = typeof event.data === "string" ? decodeOpenFrame(event.data) : Option.none();
|
|
186
|
+
if (Option.isNone(frame)) {
|
|
187
|
+
return unreadable(`${origin} answered the forward with a frame this SDK cannot read.`);
|
|
188
|
+
}
|
|
189
|
+
if (frame.value.t === "refused")
|
|
190
|
+
return fail(refusalError(frame.value));
|
|
191
|
+
settle("open");
|
|
192
|
+
resolve({ binding: frame.value.binding, forward });
|
|
193
|
+
});
|
|
194
|
+
ws.addEventListener("close", () => {
|
|
195
|
+
if (phase === "awaiting-answer") {
|
|
196
|
+
return fail(new ForwardDialError({
|
|
197
|
+
reason: "transport-unavailable",
|
|
198
|
+
message: `The forward's connection to ${origin} closed before the control plane answered.`,
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
if (phase !== "handshake")
|
|
202
|
+
return;
|
|
203
|
+
phase = "probing";
|
|
204
|
+
void failedHandshakeError(url, origin, probing.signal).then(fail);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
};
|
package/dist/facade/workspace.js
CHANGED
|
@@ -7,6 +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
11
|
import { makeInteractiveSession } from "./session.js";
|
|
11
12
|
/** The wire runtime as the SDK reports it. */
|
|
12
13
|
export const toRuntimeInfo = (runtime) => ({
|
|
@@ -23,6 +24,16 @@ export const toRuntimeInfo = (runtime) => ({
|
|
|
23
24
|
// concurrent stop while ready() polls must surface immediately, not as a 10-minute timeout.
|
|
24
25
|
// "retained" too: the executor ended and is kept for recovery; it never becomes ready.
|
|
25
26
|
const FAILED_STATUSES = new Set(["failed", "cancelled", "stopped", "retained"]);
|
|
27
|
+
const OWNER_MAP_REFUSAL_CODES = new Set(["owner-map-unsupported", "owner-map-unverified"]);
|
|
28
|
+
const readyFailureCode = (code) => {
|
|
29
|
+
if (code === "image-build-stalled")
|
|
30
|
+
return "workspace_image_build_stalled";
|
|
31
|
+
if (code === "image-build-timeout")
|
|
32
|
+
return "workspace_image_build_timeout";
|
|
33
|
+
if (code !== undefined && OWNER_MAP_REFUSAL_CODES.has(code))
|
|
34
|
+
return code;
|
|
35
|
+
return "workspace_not_ready";
|
|
36
|
+
};
|
|
26
37
|
const READY_POLL_INTERVAL_MS = 2_000;
|
|
27
38
|
/**
|
|
28
39
|
* `ready()` looks again soon, then less often: a Docker launch is ready in about four seconds, and
|
|
@@ -479,13 +490,7 @@ export const makeWorkspace = (ctx, init) => {
|
|
|
479
490
|
}
|
|
480
491
|
if (FAILED_STATUSES.has(details.status)) {
|
|
481
492
|
const reason = details.error === undefined ? "" : `: ${details.error.message}`;
|
|
482
|
-
throw new SealantError(`Workspace ${init.id} reached terminal status "${details.status}" before becoming ready${reason}`, {
|
|
483
|
-
code: details.error?.code === "image-build-stalled"
|
|
484
|
-
? "workspace_image_build_stalled"
|
|
485
|
-
: details.error?.code === "image-build-timeout"
|
|
486
|
-
? "workspace_image_build_timeout"
|
|
487
|
-
: "workspace_not_ready",
|
|
488
|
-
});
|
|
493
|
+
throw new SealantError(`Workspace ${init.id} reached terminal status "${details.status}" before becoming ready${reason}`, { code: readyFailureCode(details.error?.code) });
|
|
489
494
|
}
|
|
490
495
|
const phase = details.phase === undefined ? undefined : toWorkspacePhase(details.phase);
|
|
491
496
|
const wasBuilding = building;
|
|
@@ -796,105 +801,11 @@ export const makeWorkspace = (ctx, init) => {
|
|
|
796
801
|
sshAsRoot: async () => {
|
|
797
802
|
await ctx.runtime.run(clearWorkspaceSshUserOp(init.id, { ownerUserId: ctx.config.hostLocal.ownerUserId }));
|
|
798
803
|
},
|
|
799
|
-
forward: (port, options) => openForward(
|
|
804
|
+
forward: (port, options) => openForward({
|
|
805
|
+
baseUrl: ctx.config.baseUrl,
|
|
806
|
+
apiKey: ctx.config.apiKey,
|
|
807
|
+
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
808
|
+
}, init.id, port, options),
|
|
800
809
|
};
|
|
801
810
|
return workspace;
|
|
802
811
|
};
|
|
803
|
-
/**
|
|
804
|
-
* Open the held-WebSocket port forward (the byte-pipe data plane, mirroring
|
|
805
|
-
* the session attachment): binary frames are payload bytes in both
|
|
806
|
-
* directions; text frames are control JSON — `{"t":"eof"}` up for half-close,
|
|
807
|
-
* `{"t":"end"}` down when the remote closes. Auth rides the connect
|
|
808
|
-
* (`?token=` / `?ownerUserId=`), never per frame. The server refuses the
|
|
809
|
-
* upgrade with a plain HTTP status when nothing listens on the port, which
|
|
810
|
-
* surfaces here as the connect rejection.
|
|
811
|
-
*/
|
|
812
|
-
const openForward = (ctx, workspaceId, port, options) => {
|
|
813
|
-
const config = ctx.config;
|
|
814
|
-
const url = new URL(`/v1/workspaces/${workspaceId}/forward`, config.baseUrl);
|
|
815
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
816
|
-
url.searchParams.set("port", String(port));
|
|
817
|
-
if (options?.host !== undefined) {
|
|
818
|
-
url.searchParams.set("host", options.host);
|
|
819
|
-
}
|
|
820
|
-
if (options?.protocol === "udp") {
|
|
821
|
-
url.searchParams.set("protocol", "udp");
|
|
822
|
-
}
|
|
823
|
-
// The owner assertion always rides the URL (a service principal needs it alongside its
|
|
824
|
-
// key, and WebSocket cannot carry headers — same contract as the session attach).
|
|
825
|
-
url.searchParams.set("ownerUserId", config.hostLocal.ownerUserId);
|
|
826
|
-
if (config.apiKey !== undefined) {
|
|
827
|
-
url.searchParams.set("token", config.apiKey);
|
|
828
|
-
}
|
|
829
|
-
return new Promise((resolve, reject) => {
|
|
830
|
-
const ws = new WebSocket(url);
|
|
831
|
-
ws.binaryType = "arraybuffer";
|
|
832
|
-
// Push-queue bridging WS message events to the pull-based async iterable.
|
|
833
|
-
const pending = [];
|
|
834
|
-
let wake;
|
|
835
|
-
let finished = false;
|
|
836
|
-
const closedResolver = Promise.withResolvers();
|
|
837
|
-
const closed = closedResolver.promise;
|
|
838
|
-
const finish = (reason) => {
|
|
839
|
-
if (finished) {
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
finished = true;
|
|
843
|
-
closedResolver.resolve(reason);
|
|
844
|
-
wake?.();
|
|
845
|
-
};
|
|
846
|
-
ws.addEventListener("message", (event) => {
|
|
847
|
-
if (typeof event.data === "string") {
|
|
848
|
-
try {
|
|
849
|
-
const frame = JSON.parse(event.data);
|
|
850
|
-
if (frame.t === "end") {
|
|
851
|
-
finish("end");
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
catch {
|
|
855
|
-
// Unknown text frame — ignore.
|
|
856
|
-
}
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
pending.push(new Uint8Array(event.data));
|
|
860
|
-
wake?.();
|
|
861
|
-
});
|
|
862
|
-
ws.addEventListener("close", () => finish("closed"));
|
|
863
|
-
const output = {
|
|
864
|
-
[Symbol.asyncIterator]: () => ({
|
|
865
|
-
next: async () => {
|
|
866
|
-
for (;;) {
|
|
867
|
-
const chunk = pending.shift();
|
|
868
|
-
if (chunk !== undefined) {
|
|
869
|
-
return { done: false, value: chunk };
|
|
870
|
-
}
|
|
871
|
-
if (finished) {
|
|
872
|
-
return { done: true, value: undefined };
|
|
873
|
-
}
|
|
874
|
-
await new Promise((r) => {
|
|
875
|
-
wake = r;
|
|
876
|
-
});
|
|
877
|
-
wake = undefined;
|
|
878
|
-
}
|
|
879
|
-
},
|
|
880
|
-
}),
|
|
881
|
-
};
|
|
882
|
-
const forward = {
|
|
883
|
-
send: (input) => {
|
|
884
|
-
// Copy into a plain ArrayBuffer-backed view (WebSocket.send rejects SharedArrayBuffer views).
|
|
885
|
-
ws.send(new Uint8Array(input).buffer);
|
|
886
|
-
},
|
|
887
|
-
eof: () => {
|
|
888
|
-
ws.send(JSON.stringify({ t: "eof" }));
|
|
889
|
-
},
|
|
890
|
-
output,
|
|
891
|
-
closed,
|
|
892
|
-
close: () => {
|
|
893
|
-
finish("closed");
|
|
894
|
-
ws.close();
|
|
895
|
-
},
|
|
896
|
-
};
|
|
897
|
-
ws.addEventListener("open", () => resolve(forward), { once: true });
|
|
898
|
-
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 });
|
|
899
|
-
});
|
|
900
|
-
};
|
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 { SealantApiError, SealantError, SealantNotImplementedError, SealantRuntimeError, } from "./errors.js";
|
|
19
|
+
export { ForwardDialError, type ForwardDialFailure, type ForwardDialReason, 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 { SealantApiError, SealantError, SealantNotImplementedError, SealantRuntimeError, } from "./errors.js";
|
|
19
|
+
export { ForwardDialError, 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
|
@@ -1064,11 +1064,14 @@ export interface Workspace {
|
|
|
1064
1064
|
* Rejects with `workspace_ready_timeout` past the readiness bound, `workspace_image_build_timeout`
|
|
1065
1065
|
* past the image-build bound (the caller's, or the control plane's own), `workspace_image_build_stalled` when the build stopped making
|
|
1066
1066
|
* progress (with the step it stopped on), and `workspace_not_ready` when the launch ended
|
|
1067
|
-
* otherwise (with the control plane's reason
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1067
|
+
* otherwise (with the control plane's reason; a runtime's `unsupported-runtime-requirement`
|
|
1068
|
+
* refusal is one). A per-person launch the worker refused for its owner map rejects with the
|
|
1069
|
+
* refusal's own code: `owner-map-unsupported` when the image cannot apply the owner map,
|
|
1070
|
+
* `owner-map-unverified` when Core cannot tell whether it does.
|
|
1071
|
+
* When the handle came from `workspaces.create()` and `ready()` gives up on a bound, a stop is
|
|
1072
|
+
* requested before the error is thrown, so an abandoned launch does not keep running to its cap;
|
|
1073
|
+
* the error says whether the request was accepted (not that the workspace stopped). Options given
|
|
1074
|
+
* here win over the ones given to `create()`.
|
|
1072
1075
|
*/
|
|
1073
1076
|
ready(options?: WorkspaceReadyOptions): Promise<this>;
|
|
1074
1077
|
/**
|
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.758",
|
|
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.758"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"effect": "^4.0.5"
|
|
33
33
|
},
|
|
34
|
-
"gitHead": "
|
|
34
|
+
"gitHead": "cad5836dece75c30a8aa91978d233cd1c1f7064d",
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
|
37
37
|
}
|