mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-29 14:16:16 +00:00
Compare commits
1
Commits
v2
...
browser-contracts
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7a1b3011e |
@@ -0,0 +1,79 @@
|
||||
export * as BrowserControlProtocol from "./browser-control.js"
|
||||
|
||||
import { BrowserControl } from "@opencode-ai/schema/browser-control"
|
||||
import { Effect, Schema } from "effect"
|
||||
|
||||
export const Path = "/api/browser/control"
|
||||
export const Subprotocol = "opencode.browser.control.v1"
|
||||
export const MaxMessageBytes = 8 * 1_024 * 1_024
|
||||
|
||||
class MessageError extends Schema.TaggedErrorClass<MessageError>()("BrowserControlProtocol.MessageError", {
|
||||
kind: Schema.Literals(["invalid", "too_large"]),
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect()),
|
||||
}) {}
|
||||
|
||||
const decoder = new TextDecoder("utf-8", { fatal: true })
|
||||
const encoder = new TextEncoder()
|
||||
const encodeClient = Schema.encodeSync(Schema.fromJsonString(BrowserControl.FromClient))
|
||||
const encodeServer = Schema.encodeSync(Schema.fromJsonString(BrowserControl.FromServer))
|
||||
const decodeClient = Schema.decodeUnknownEffect(Schema.fromJsonString(BrowserControl.FromClient), {
|
||||
errors: "all",
|
||||
onExcessProperty: "error",
|
||||
})
|
||||
const decodeServer = Schema.decodeUnknownEffect(Schema.fromJsonString(BrowserControl.FromServer), {
|
||||
errors: "all",
|
||||
onExcessProperty: "error",
|
||||
})
|
||||
|
||||
export function encodeFromClient(input: BrowserControl.FromClient) {
|
||||
return encode(input, encodeClient)
|
||||
}
|
||||
|
||||
export function encodeFromServer(input: BrowserControl.FromServer) {
|
||||
return encode(input, encodeServer)
|
||||
}
|
||||
|
||||
function encode<Message>(input: Message, encodeMessage: (input: Message) => string) {
|
||||
const output = encodeMessage(input)
|
||||
if (encoder.encode(output).byteLength > MaxMessageBytes) {
|
||||
throw new RangeError(`Browser control message must not exceed ${MaxMessageBytes} bytes.`)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
export function decodeFromClient(input: string | Uint8Array) {
|
||||
return decode(input, decodeClient)
|
||||
}
|
||||
|
||||
export function decodeFromServer(input: string | Uint8Array) {
|
||||
return decode(input, decodeServer)
|
||||
}
|
||||
|
||||
function decode<Message>(
|
||||
input: string | Uint8Array,
|
||||
decodeMessage: (input: unknown) => Effect.Effect<Message, unknown>,
|
||||
): Effect.Effect<Message, MessageError> {
|
||||
if (typeof input === "string" && encoder.encode(input).byteLength > MaxMessageBytes) {
|
||||
return Effect.fail(new MessageError({ kind: "too_large", message: "Browser control message is too large." }))
|
||||
}
|
||||
if (typeof input !== "string" && input.byteLength > MaxMessageBytes) {
|
||||
return Effect.fail(new MessageError({ kind: "too_large", message: "Browser control message is too large." }))
|
||||
}
|
||||
const text =
|
||||
typeof input === "string"
|
||||
? Effect.succeed(input)
|
||||
: Effect.try({
|
||||
try: () => decoder.decode(input),
|
||||
catch: (cause) =>
|
||||
new MessageError({ kind: "invalid", message: "Browser control message is not valid UTF-8.", cause }),
|
||||
})
|
||||
return text.pipe(
|
||||
Effect.flatMap(decodeMessage),
|
||||
Effect.mapError((cause) =>
|
||||
cause instanceof MessageError
|
||||
? cause
|
||||
: new MessageError({ kind: "invalid", message: "Browser control message is invalid.", cause }),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
export * as BrowserTunnelProtocol from "./browser-tunnel.js"
|
||||
|
||||
import { BrowserTunnel } from "@opencode-ai/schema/browser-tunnel"
|
||||
import { Effect, Schema } from "effect"
|
||||
|
||||
export const Path = "/api/browser/tunnel"
|
||||
export const Subprotocol = "opencode.browser.tunnel.v1"
|
||||
export const MaxFrameBytes = 64 * 1_024
|
||||
export const MaxHandshakeBytes = 16 * 1_024
|
||||
|
||||
class MessageError extends Schema.TaggedErrorClass<MessageError>()("BrowserTunnelProtocol.MessageError", {
|
||||
kind: Schema.Literals(["invalid", "too_large"]),
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect()),
|
||||
}) {}
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
const decoder = new TextDecoder("utf-8", { fatal: true })
|
||||
const encodeClient = Schema.encodeSync(Schema.fromJsonString(BrowserTunnel.FromClient))
|
||||
const encodeServer = Schema.encodeSync(Schema.fromJsonString(BrowserTunnel.FromServer))
|
||||
const decodeClient = Schema.decodeUnknownEffect(Schema.fromJsonString(BrowserTunnel.FromClient), {
|
||||
errors: "all",
|
||||
onExcessProperty: "error",
|
||||
})
|
||||
const decodeServer = Schema.decodeUnknownEffect(Schema.fromJsonString(BrowserTunnel.FromServer), {
|
||||
errors: "all",
|
||||
onExcessProperty: "error",
|
||||
})
|
||||
|
||||
export function encodeFromClient(input: BrowserTunnel.FromClient) {
|
||||
return encode(encodeClient(input))
|
||||
}
|
||||
|
||||
export function encodeFromServer(input: BrowserTunnel.FromServer) {
|
||||
return encode(encodeServer(input))
|
||||
}
|
||||
|
||||
function encode(input: string) {
|
||||
if (encoder.encode(input).byteLength > MaxHandshakeBytes) {
|
||||
throw new RangeError(`Browser tunnel handshake must not exceed ${MaxHandshakeBytes} bytes.`)
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
export function decodeFromClient(input: string | Uint8Array) {
|
||||
return decode(input, decodeClient)
|
||||
}
|
||||
|
||||
export function decodeFromServer(input: string | Uint8Array) {
|
||||
return decode(input, decodeServer)
|
||||
}
|
||||
|
||||
function decode<Message>(
|
||||
input: string | Uint8Array,
|
||||
decodeMessage: (input: unknown) => Effect.Effect<Message, unknown>,
|
||||
): Effect.Effect<Message, MessageError> {
|
||||
if ((typeof input === "string" ? encoder.encode(input).byteLength : input.byteLength) > MaxHandshakeBytes) {
|
||||
return Effect.fail(new MessageError({ kind: "too_large", message: "Browser tunnel handshake is too large." }))
|
||||
}
|
||||
const text =
|
||||
typeof input === "string"
|
||||
? Effect.succeed(input)
|
||||
: Effect.try({
|
||||
try: () => decoder.decode(input),
|
||||
catch: (cause) => new MessageError({ kind: "invalid", message: "Invalid tunnel handshake UTF-8.", cause }),
|
||||
})
|
||||
return text.pipe(
|
||||
Effect.flatMap(decodeMessage),
|
||||
Effect.mapError((cause) =>
|
||||
cause instanceof MessageError
|
||||
? cause
|
||||
: new MessageError({ kind: "invalid", message: "Browser tunnel handshake is invalid.", cause }),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { ConflictError, ServiceUnavailableError } from "../errors.js"
|
||||
import { BrowserControlProtocol } from "../browser-control.js"
|
||||
import { BrowserTunnelProtocol } from "../browser-tunnel.js"
|
||||
import { HeaderOnlyAuthorization } from "../middleware/authorization.js"
|
||||
|
||||
const websocket = (identifier: string, summary: string, description: string, subprotocol: string) =>
|
||||
OpenApi.annotations({
|
||||
identifier,
|
||||
summary,
|
||||
description,
|
||||
transform: (operation) => ({
|
||||
...operation,
|
||||
"x-websocket": true,
|
||||
"x-websocket-subprotocol": subprotocol,
|
||||
responses: {
|
||||
...operation.responses,
|
||||
403: { description: "WebSocket Origin is not allowed." },
|
||||
426: { description: `WebSocket subprotocol ${subprotocol} is required.` },
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
export const BrowserGroup = HttpApiGroup.make("server.browser")
|
||||
.add(
|
||||
HttpApiEndpoint.get("browser.control.connect", BrowserControlProtocol.Path, {
|
||||
success: Schema.Boolean,
|
||||
error: ConflictError,
|
||||
})
|
||||
.annotate(HeaderOnlyAuthorization, true)
|
||||
.annotate(OpenApi.Exclude, true)
|
||||
.annotateMerge(
|
||||
websocket(
|
||||
"v2.browser.control.connect",
|
||||
"Connect Session browser host",
|
||||
"Establish an authenticated WebSocket controlling the browser attachment for one Session.",
|
||||
BrowserControlProtocol.Subprotocol,
|
||||
),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("browser.tunnel.connect", BrowserTunnelProtocol.Path, {
|
||||
success: Schema.Boolean,
|
||||
error: ServiceUnavailableError,
|
||||
})
|
||||
.annotate(HeaderOnlyAuthorization, true)
|
||||
.annotate(OpenApi.Exclude, true)
|
||||
.annotateMerge(
|
||||
websocket(
|
||||
"v2.browser.tunnel.connect",
|
||||
"Open browser network tunnel",
|
||||
"Establish an authenticated WebSocket carrying one TCP stream dialed from the OpenCode server.",
|
||||
BrowserTunnelProtocol.Subprotocol,
|
||||
),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "browser",
|
||||
description: "Desktop browser host control and server-network tunnel routes.",
|
||||
}),
|
||||
)
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Context } from "effect"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
import { UnauthorizedError } from "../errors.js"
|
||||
|
||||
export const HeaderOnlyAuthorization = Context.Reference<boolean>("@opencode/HttpApiAuthorization/HeaderOnly", {
|
||||
defaultValue: () => false,
|
||||
})
|
||||
|
||||
export class Authorization extends HttpApiMiddleware.Service<Authorization>()("@opencode/HttpApiAuthorization", {
|
||||
error: UnauthorizedError,
|
||||
}) {}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
export * as BrowserControl from "./browser-control.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Browser } from "./browser.js"
|
||||
import { ascending } from "./identifier.js"
|
||||
import { SessionID } from "./session-id.js"
|
||||
import { statics } from "./schema.js"
|
||||
|
||||
const RequestIDSchema = Schema.String.check(Schema.isPattern(/^brr_[0-9A-Za-z]+$/))
|
||||
.pipe(Schema.brand("BrowserControl.RequestID"))
|
||||
.annotate({ identifier: "BrowserControl.RequestID" })
|
||||
|
||||
export const RequestID = RequestIDSchema.pipe(
|
||||
statics((schema: typeof RequestIDSchema) => ({
|
||||
create: () => schema.make("brr_" + ascending()),
|
||||
})),
|
||||
)
|
||||
export type RequestID = typeof RequestID.Type
|
||||
|
||||
const Register = Schema.Struct({
|
||||
type: Schema.Literal("browser.control.register"),
|
||||
sessionID: SessionID,
|
||||
})
|
||||
|
||||
const Attach = Schema.Struct({
|
||||
type: Schema.Literal("browser.control.attach"),
|
||||
leaseID: Browser.LeaseID,
|
||||
state: Browser.State,
|
||||
})
|
||||
|
||||
const State = Schema.Struct({
|
||||
type: Schema.Literal("browser.control.state"),
|
||||
leaseID: Browser.LeaseID,
|
||||
state: Browser.State,
|
||||
})
|
||||
|
||||
const Detach = Schema.Struct({
|
||||
type: Schema.Literal("browser.control.detach"),
|
||||
leaseID: Browser.LeaseID,
|
||||
})
|
||||
|
||||
const Response = Schema.Struct({
|
||||
type: Schema.Literal("browser.control.response"),
|
||||
requestID: RequestID,
|
||||
leaseID: Browser.LeaseID,
|
||||
outcome: Browser.Outcome,
|
||||
})
|
||||
|
||||
const Registered = Schema.Struct({ type: Schema.Literal("browser.control.registered") })
|
||||
const Open = Schema.Struct({ type: Schema.Literal("browser.control.open") })
|
||||
|
||||
const Attached = Schema.Struct({
|
||||
type: Schema.Literal("browser.control.attached"),
|
||||
leaseID: Browser.LeaseID,
|
||||
})
|
||||
|
||||
const Request = Schema.Struct({
|
||||
type: Schema.Literal("browser.control.request"),
|
||||
requestID: RequestID,
|
||||
leaseID: Browser.LeaseID,
|
||||
command: Browser.Command,
|
||||
})
|
||||
|
||||
const Cancel = Schema.Struct({
|
||||
type: Schema.Literal("browser.control.cancel"),
|
||||
requestID: RequestID,
|
||||
leaseID: Browser.LeaseID,
|
||||
})
|
||||
|
||||
export const FromClient = Schema.Union([Register, Attach, State, Detach, Response])
|
||||
.pipe(Schema.toTaggedUnion("type"))
|
||||
.annotate({ identifier: "BrowserControl.FromClient" })
|
||||
export type FromClient = typeof FromClient.Type
|
||||
|
||||
export const FromServer = Schema.Union([Registered, Open, Attached, Request, Cancel])
|
||||
.pipe(Schema.toTaggedUnion("type"))
|
||||
.annotate({ identifier: "BrowserControl.FromServer" })
|
||||
export type FromServer = typeof FromServer.Type
|
||||
@@ -0,0 +1,55 @@
|
||||
export * as BrowserTunnel from "./browser-tunnel.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Browser } from "./browser.js"
|
||||
import { SessionID } from "./session-id.js"
|
||||
|
||||
export const Host = Schema.NonEmptyString.check(Schema.isMaxLength(253), Schema.isPattern(/^[^\s/?#]+$/))
|
||||
.pipe(Schema.brand("BrowserTunnel.Host"))
|
||||
.annotate({ identifier: "BrowserTunnel.Host" })
|
||||
export type Host = typeof Host.Type
|
||||
|
||||
export const Port = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65_535 }))
|
||||
.pipe(Schema.brand("BrowserTunnel.Port"))
|
||||
.annotate({ identifier: "BrowserTunnel.Port" })
|
||||
export type Port = typeof Port.Type
|
||||
|
||||
export interface Target extends Schema.Schema.Type<typeof Target> {}
|
||||
export const Target = Schema.Struct({
|
||||
host: Host,
|
||||
port: Port,
|
||||
}).annotate({ identifier: "BrowserTunnel.Target" })
|
||||
|
||||
const Open = Schema.Struct({
|
||||
type: Schema.Literal("browser.tunnel.open"),
|
||||
sessionID: SessionID,
|
||||
leaseID: Browser.LeaseID,
|
||||
target: Target,
|
||||
}).annotate({ identifier: "BrowserTunnel.Open" })
|
||||
|
||||
const Opened = Schema.Struct({
|
||||
type: Schema.Literal("browser.tunnel.opened"),
|
||||
}).annotate({ identifier: "BrowserTunnel.Opened" })
|
||||
|
||||
export const OpenErrorCode = Schema.Literals([
|
||||
"invalid_open",
|
||||
"not_attached",
|
||||
"stale_lease",
|
||||
"connect_failed",
|
||||
"connect_timeout",
|
||||
]).annotate({ identifier: "BrowserTunnel.OpenErrorCode" })
|
||||
export type OpenErrorCode = typeof OpenErrorCode.Type
|
||||
|
||||
const Rejected = Schema.Struct({
|
||||
type: Schema.Literal("browser.tunnel.rejected"),
|
||||
code: OpenErrorCode,
|
||||
message: Schema.String.check(Schema.isMaxLength(1_024)),
|
||||
}).annotate({ identifier: "BrowserTunnel.Rejected" })
|
||||
|
||||
export const FromClient = Open.annotate({ identifier: "BrowserTunnel.FromClient" })
|
||||
export type FromClient = typeof FromClient.Type
|
||||
|
||||
export const FromServer = Schema.Union([Opened, Rejected])
|
||||
.pipe(Schema.toTaggedUnion("type"))
|
||||
.annotate({ identifier: "BrowserTunnel.FromServer" })
|
||||
export type FromServer = typeof FromServer.Type
|
||||
@@ -0,0 +1,163 @@
|
||||
export * as Browser from "./browser.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { ascending } from "./identifier.js"
|
||||
import { NonNegativeInt, PositiveInt, statics } from "./schema.js"
|
||||
|
||||
const LeaseIDSchema = Schema.String.check(Schema.isPattern(/^brl_[0-9A-Za-z]+$/))
|
||||
.pipe(Schema.brand("Browser.LeaseID"))
|
||||
.annotate({ identifier: "Browser.LeaseID" })
|
||||
|
||||
export const LeaseID = LeaseIDSchema.pipe(
|
||||
statics((schema: typeof LeaseIDSchema) => ({
|
||||
create: () => schema.make("brl_" + ascending()),
|
||||
})),
|
||||
)
|
||||
export type LeaseID = typeof LeaseID.Type
|
||||
|
||||
export const Ref = Schema.String.check(Schema.isPattern(/^e[1-9][0-9]*$/))
|
||||
.pipe(Schema.brand("Browser.Ref"))
|
||||
.annotate({ identifier: "Browser.Ref" })
|
||||
export type Ref = typeof Ref.Type
|
||||
|
||||
export interface State extends Schema.Schema.Type<typeof State> {}
|
||||
export const State = Schema.Struct({
|
||||
url: Schema.String.check(Schema.isMaxLength(16_384)),
|
||||
title: Schema.String.check(Schema.isMaxLength(1_024)),
|
||||
loading: Schema.Boolean,
|
||||
canGoBack: Schema.Boolean,
|
||||
canGoForward: Schema.Boolean,
|
||||
generation: NonNegativeInt,
|
||||
}).annotate({ identifier: "Browser.State" })
|
||||
|
||||
export const Key = Schema.Literals([
|
||||
"Enter",
|
||||
"Tab",
|
||||
"Escape",
|
||||
"Backspace",
|
||||
"Delete",
|
||||
"ArrowUp",
|
||||
"ArrowDown",
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"Home",
|
||||
"End",
|
||||
"Space",
|
||||
]).annotate({ identifier: "Browser.Key" })
|
||||
export type Key = typeof Key.Type
|
||||
|
||||
export const Direction = Schema.Literals(["up", "down", "left", "right"]).annotate({
|
||||
identifier: "Browser.Direction",
|
||||
})
|
||||
export type Direction = typeof Direction.Type
|
||||
|
||||
const generation = { generation: NonNegativeInt }
|
||||
|
||||
export const Command = Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("navigate"),
|
||||
url: Schema.String.check(Schema.isMaxLength(16_384)),
|
||||
...generation,
|
||||
}),
|
||||
Schema.Struct({ type: Schema.Literal("snapshot"), ...generation }),
|
||||
Schema.Struct({ type: Schema.Literal("click"), ref: Ref, ...generation }),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("fill"),
|
||||
ref: Ref,
|
||||
text: Schema.String.check(Schema.isMaxLength(10_000)),
|
||||
...generation,
|
||||
}),
|
||||
Schema.Struct({ type: Schema.Literal("press"), key: Key, ...generation }),
|
||||
Schema.Struct({ type: Schema.Literal("scroll"), direction: Direction, pixels: PositiveInt, ...generation }),
|
||||
Schema.Struct({ type: Schema.Literal("screenshot"), ...generation }),
|
||||
])
|
||||
.pipe(Schema.toTaggedUnion("type"))
|
||||
.annotate({ identifier: "Browser.Command" })
|
||||
export type Command = typeof Command.Type
|
||||
|
||||
const NavigateResult = Schema.Struct({
|
||||
type: Schema.Literal("navigate"),
|
||||
state: State,
|
||||
}).annotate({ identifier: "Browser.NavigateResult" })
|
||||
|
||||
const SnapshotResult = Schema.Struct({
|
||||
type: Schema.Literal("snapshot"),
|
||||
state: State,
|
||||
format: Schema.Literal("opencode.semantic.v1"),
|
||||
content: Schema.String.check(Schema.isMaxLength(100_000)),
|
||||
}).annotate({ identifier: "Browser.SnapshotResult" })
|
||||
|
||||
const ClickResult = Schema.Struct({
|
||||
type: Schema.Literal("click"),
|
||||
state: State,
|
||||
}).annotate({ identifier: "Browser.ClickResult" })
|
||||
|
||||
const FillResult = Schema.Struct({
|
||||
type: Schema.Literal("fill"),
|
||||
state: State,
|
||||
}).annotate({ identifier: "Browser.FillResult" })
|
||||
|
||||
const PressResult = Schema.Struct({
|
||||
type: Schema.Literal("press"),
|
||||
state: State,
|
||||
}).annotate({ identifier: "Browser.PressResult" })
|
||||
|
||||
const ScrollResult = Schema.Struct({
|
||||
type: Schema.Literal("scroll"),
|
||||
state: State,
|
||||
}).annotate({ identifier: "Browser.ScrollResult" })
|
||||
|
||||
const ScreenshotResult = Schema.Struct({
|
||||
type: Schema.Literal("screenshot"),
|
||||
state: State,
|
||||
mediaType: Schema.Literal("image/png"),
|
||||
data: Schema.Uint8ArrayFromBase64.check(Schema.isMaxLength(5 * 1_024 * 1_024)),
|
||||
width: PositiveInt,
|
||||
height: PositiveInt,
|
||||
}).annotate({ identifier: "Browser.ScreenshotResult" })
|
||||
|
||||
export const Result = Schema.Union([
|
||||
NavigateResult,
|
||||
SnapshotResult,
|
||||
ClickResult,
|
||||
FillResult,
|
||||
PressResult,
|
||||
ScrollResult,
|
||||
ScreenshotResult,
|
||||
])
|
||||
.pipe(Schema.toTaggedUnion("type"))
|
||||
.annotate({ identifier: "Browser.Result" })
|
||||
export type Result = typeof Result.Type
|
||||
|
||||
export const ErrorCode = Schema.Literals([
|
||||
"not_attached",
|
||||
"stale_ref",
|
||||
"invalid_url",
|
||||
"navigation_failed",
|
||||
"timeout",
|
||||
"aborted",
|
||||
"page_crashed",
|
||||
"result_too_large",
|
||||
"overloaded",
|
||||
"protocol",
|
||||
"internal",
|
||||
]).annotate({ identifier: "Browser.ErrorCode" })
|
||||
export type ErrorCode = typeof ErrorCode.Type
|
||||
|
||||
const Failure = Schema.Struct({
|
||||
type: Schema.Literal("failure"),
|
||||
code: ErrorCode,
|
||||
message: Schema.String.check(Schema.isMaxLength(1_024)),
|
||||
}).annotate({ identifier: "Browser.Failure" })
|
||||
|
||||
const Success = Schema.Struct({
|
||||
type: Schema.Literal("success"),
|
||||
result: Result,
|
||||
}).annotate({ identifier: "Browser.Success" })
|
||||
|
||||
export const Outcome = Schema.Union([Success, Failure])
|
||||
.pipe(Schema.toTaggedUnion("type"))
|
||||
.annotate({ identifier: "Browser.Outcome" })
|
||||
export type Outcome = typeof Outcome.Type
|
||||
@@ -1,4 +1,5 @@
|
||||
export { Agent } from "./agent.js"
|
||||
export { Browser } from "./browser.js"
|
||||
export { Command } from "./command.js"
|
||||
export { Config } from "./config.js"
|
||||
export { Connection } from "./connection.js"
|
||||
|
||||
Reference in New Issue
Block a user