effect-browserbase 0.2.0-beta.0 → 0.2.0-beta.1
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/README.md +47 -4
- package/dist/Account.d.mts +15 -15
- package/dist/Account.mjs +3 -3
- package/dist/{Acquisition-D8o4jtCg.mjs → Acquisition-CAsTTXp4.mjs} +9 -8
- package/dist/Acquisition-CAsTTXp4.mjs.map +1 -0
- package/dist/Agents-5HpTp9YE.d.mts +155 -0
- package/dist/Agents.d.mts +3 -154
- package/dist/Agents.mjs +3 -3
- package/dist/Allocation.d.mts +4 -4
- package/dist/Allocation.mjs +1 -1
- package/dist/Browser-DpSajNai.mjs +270 -0
- package/dist/Browser-DpSajNai.mjs.map +1 -0
- package/dist/Browser.d.mts +6 -5
- package/dist/Browser.mjs +2 -266
- package/dist/BrowserBinding.mjs +3 -3
- package/dist/BrowserBinding.mjs.map +1 -1
- package/dist/Certificates-D4NQmyeA.d.mts +34 -0
- package/dist/Certificates.d.mts +3 -33
- package/dist/Certificates.mjs +1 -1
- package/dist/{Client-BQteUePT.mjs → Client-4RMKIpL2.mjs} +11 -9
- package/dist/Client-4RMKIpL2.mjs.map +1 -0
- package/dist/Client-B_f6bBLT.d.mts +52 -0
- package/dist/Client.d.mts +3 -51
- package/dist/Client.mjs +1 -1
- package/dist/{Connection-DcvJZEdc.mjs → Connection-sxyxtwlk.mjs} +2 -11
- package/dist/Connection-sxyxtwlk.mjs.map +1 -0
- package/dist/ContextCoordination.d.mts +1 -1
- package/dist/ContextCoordination.mjs +1 -1
- package/dist/ContextCoordination.mjs.map +1 -1
- package/dist/{ContextWriter-BrQmoXRg.mjs → ContextWriter-oKWFLHuP.mjs} +8 -12
- package/dist/ContextWriter-oKWFLHuP.mjs.map +1 -0
- package/dist/Contexts-DqdL7cyX.d.mts +31 -0
- package/dist/Contexts.d.mts +3 -30
- package/dist/Contexts.mjs +2 -2
- package/dist/Contexts.mjs.map +1 -1
- package/dist/Downloads-CdhiCO5X.d.mts +43 -0
- package/dist/Downloads.d.mts +3 -42
- package/dist/Downloads.mjs +2 -2
- package/dist/Downloads.mjs.map +1 -1
- package/dist/Extensions-DBXuVM1f.d.mts +43 -0
- package/dist/{Extensions-nMNMPGCc.mjs → Extensions-Dg3jHOf0.mjs} +2 -2
- package/dist/Extensions-Dg3jHOf0.mjs.map +1 -0
- package/dist/Extensions.d.mts +3 -42
- package/dist/Extensions.mjs +1 -1
- package/dist/Functions-CsKMMUuZ.d.mts +226 -0
- package/dist/Functions.d.mts +3 -225
- package/dist/Functions.mjs +3 -3
- package/dist/{Launch-m0fxzJ49.mjs → Launch-cAdE2rrZ.mjs} +12 -8
- package/dist/Launch-cAdE2rrZ.mjs.map +1 -0
- package/dist/Launch.d.mts +1 -4
- package/dist/Launch.mjs +1 -2
- package/dist/Launch.mjs.map +1 -1
- package/dist/PageFetch-CAMqb5m8.d.mts +39 -0
- package/dist/PageFetch.d.mts +3 -38
- package/dist/PageFetch.mjs +1 -1
- package/dist/Projects-tm5bTDH0.d.mts +38 -0
- package/dist/Projects.d.mts +3 -37
- package/dist/Projects.mjs +1 -1
- package/dist/Recordings-bVI33sTq.d.mts +27 -0
- package/dist/Recordings.d.mts +3 -26
- package/dist/Recordings.mjs +2 -2
- package/dist/Replays-Bo-9xeLz.d.mts +27 -0
- package/dist/Replays.d.mts +3 -26
- package/dist/Replays.mjs +2 -2
- package/dist/Search-1g6TUem7.d.mts +37 -0
- package/dist/Search.d.mts +3 -36
- package/dist/Search.mjs +1 -1
- package/dist/{Sessions-tFJYaXFq.d.mts → Sessions-BpZgjteE.d.mts} +2 -2
- package/dist/{Sessions-DoQGg9nB.mjs → Sessions-suOLSym9.mjs} +2 -2
- package/dist/Sessions-suOLSym9.mjs.map +1 -0
- package/dist/Sessions.d.mts +1 -1
- package/dist/Sessions.mjs +1 -1
- package/dist/Testing.d.mts +119 -0
- package/dist/Testing.mjs +206 -0
- package/dist/Testing.mjs.map +1 -0
- package/dist/Uploads-C2neBEpj.d.mts +28 -0
- package/dist/Uploads.d.mts +3 -27
- package/dist/Uploads.mjs +2 -2
- package/dist/Webhooks-BoeRAxzG.d.mts +59 -0
- package/dist/Webhooks.d.mts +3 -58
- package/dist/Webhooks.mjs +1 -1
- package/dist/{WriterFacts-5CPBGG1P.d.mts → WriterFacts-BBJRDZYY.d.mts} +5 -13
- package/package.json +6 -2
- package/dist/Acquisition-D8o4jtCg.mjs.map +0 -1
- package/dist/Browser.mjs.map +0 -1
- package/dist/Client-BQteUePT.mjs.map +0 -1
- package/dist/Connection-DcvJZEdc.mjs.map +0 -1
- package/dist/ContextWriter-BrQmoXRg.mjs.map +0 -1
- package/dist/Extensions-nMNMPGCc.mjs.map +0 -1
- package/dist/Launch-m0fxzJ49.mjs.map +0 -1
- package/dist/Sessions-DoQGg9nB.mjs.map +0 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { ClientError } from "./Errors.mjs";
|
|
2
|
+
import { r as SessionStatus } from "./SessionData-5xYZDshJ.mjs";
|
|
3
|
+
import { Services } from "./Account.mjs";
|
|
4
|
+
import { LaunchRecipe } from "./Launch.mjs";
|
|
5
|
+
import { BrowserOptions, BrowserbaseBrowser } from "./Browser.mjs";
|
|
6
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
7
|
+
import { BrowserError } from "effect-browser/errors";
|
|
8
|
+
import * as BrowserTesting from "effect-browser/testing";
|
|
9
|
+
//#region src/Testing.d.ts
|
|
10
|
+
/**
|
|
11
|
+
* How the scripted control plane answers. Everything else about a session is what the real
|
|
12
|
+
* `Client`, `Sessions`, allocation and cleanup code makes of those answers.
|
|
13
|
+
*/
|
|
14
|
+
export declare const ProviderScript: Schema.Struct<{
|
|
15
|
+
readonly projectId: Schema.optionalKey<Schema.NonEmptyString>;
|
|
16
|
+
/**
|
|
17
|
+
* The creation reply: `Accept` allocates a session (running unless told otherwise), `Reject`
|
|
18
|
+
* refuses with a status the real transport classifies, `Malformed` replies without an
|
|
19
|
+
* identity, and `Lost` fails the request after it was sent, so the outcome is unknown.
|
|
20
|
+
*/
|
|
21
|
+
readonly create: Schema.optionalKey<Schema.Union<readonly [Schema.TaggedStruct<"Accept", {
|
|
22
|
+
readonly status: Schema.optionalKey<Schema.Literals<readonly ["PENDING", "RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"]>>;
|
|
23
|
+
}>, Schema.TaggedStruct<"Reject", {
|
|
24
|
+
readonly status: Schema.Int;
|
|
25
|
+
readonly retryAfterMillis: Schema.optionalKey<Schema.Natural>;
|
|
26
|
+
}>, Schema.TaggedStruct<"Malformed", {}>, Schema.TaggedStruct<"Lost", {}>]>>;
|
|
27
|
+
/** What a release request does: the session ends, stays running, or the request fails. */
|
|
28
|
+
readonly release: Schema.optionalKey<Schema.Literals<readonly ["confirmed", "pending", "failed"]>>;
|
|
29
|
+
readonly liveView: Schema.optionalKey<Schema.Literals<readonly ["issued", "denied"]>>;
|
|
30
|
+
readonly region: Schema.optionalKey<Schema.Literals<readonly ["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"]>>;
|
|
31
|
+
/**
|
|
32
|
+
* Whose identity a session's retrieval and release replies carry: its own, another session's
|
|
33
|
+
* or another project's. The real client refuses a reply it cannot match, so a test can see
|
|
34
|
+
* what allocation, connection and cleanup make of a provider that answers for someone else.
|
|
35
|
+
*/
|
|
36
|
+
readonly identity: Schema.optionalKey<Schema.Literals<readonly ["own", "foreign-session", "foreign-project"]>>;
|
|
37
|
+
}>;
|
|
38
|
+
export type ProviderScript = typeof ProviderScript.Type;
|
|
39
|
+
export interface ProviderCall {
|
|
40
|
+
readonly method: string;
|
|
41
|
+
readonly path: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ProviderSessionRecord {
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly status: SessionStatus;
|
|
46
|
+
readonly releaseRequests: number;
|
|
47
|
+
readonly uploads: ReadonlyArray<string>;
|
|
48
|
+
}
|
|
49
|
+
export interface ProviderControl {
|
|
50
|
+
readonly projectId: string;
|
|
51
|
+
readonly calls: Effect.Effect<ReadonlyArray<ProviderCall>>;
|
|
52
|
+
readonly sessions: Effect.Effect<ReadonlyArray<ProviderSessionRecord>>;
|
|
53
|
+
/** Move a session as the provider might while it is connected, for example to `TIMED_OUT`. */
|
|
54
|
+
readonly setStatus: (sessionId: string, status: SessionStatus) => Effect.Effect<void>;
|
|
55
|
+
/** Marker strings the double uses where real secrets would be, to grep evidence for leaks. */
|
|
56
|
+
readonly secrets: {
|
|
57
|
+
readonly apiKey: string;
|
|
58
|
+
readonly connectUrl: string;
|
|
59
|
+
readonly liveView: string;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** A scripted control plane: a `FetchHttpClient.Fetch` and the handle that inspects it. */
|
|
63
|
+
export interface ScriptedProvider {
|
|
64
|
+
readonly fetch: typeof globalThis.fetch;
|
|
65
|
+
readonly control: ProviderControl;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The reviewed session subset of the provider API, answered in memory: creation, retrieval,
|
|
69
|
+
* listing, release, Live View issuance and uploads. Provide it as `FetchHttpClient.Fetch`
|
|
70
|
+
* beneath the real `BrowserbaseClient.layer`, or use `layer` below, which does that.
|
|
71
|
+
*/
|
|
72
|
+
export declare const provider: (script?: {
|
|
73
|
+
readonly projectId?: string | undefined;
|
|
74
|
+
readonly create?: {
|
|
75
|
+
readonly _tag: "Accept";
|
|
76
|
+
readonly status?: "COMPLETED" | "ERROR" | "PENDING" | "RUNNING" | "TIMED_OUT" | undefined;
|
|
77
|
+
} | {
|
|
78
|
+
readonly _tag: "Reject";
|
|
79
|
+
readonly status: number;
|
|
80
|
+
readonly retryAfterMillis?: number | undefined;
|
|
81
|
+
} | {
|
|
82
|
+
readonly _tag: "Malformed";
|
|
83
|
+
} | {
|
|
84
|
+
readonly _tag: "Lost";
|
|
85
|
+
} | undefined;
|
|
86
|
+
readonly release?: "confirmed" | "failed" | "pending" | undefined;
|
|
87
|
+
readonly liveView?: "denied" | "issued" | undefined;
|
|
88
|
+
readonly region?: "ap-southeast-1" | "eu-central-1" | "us-east-1" | "us-west-2" | undefined;
|
|
89
|
+
readonly identity?: "foreign-project" | "foreign-session" | "own" | undefined;
|
|
90
|
+
} | undefined) => Effect.Effect<ScriptedProvider, ClientError, never>;
|
|
91
|
+
declare const ScriptedBrowserbase_base: Context.ServiceClass<ScriptedBrowserbase, "effect-browserbase/testing/ScriptedBrowserbase", {
|
|
92
|
+
readonly provider: ProviderControl;
|
|
93
|
+
/**
|
|
94
|
+
* One control for each scripted browser the Layer connected to, in first-connection order:
|
|
95
|
+
* one per provider session, which a keep-alive reconnection or a borrowed attachment reaches
|
|
96
|
+
* again.
|
|
97
|
+
*/
|
|
98
|
+
readonly browsers: Effect.Effect<ReadonlyArray<BrowserTesting.ScriptedControl>>;
|
|
99
|
+
}>;
|
|
100
|
+
/** The scripted provider and the scripted engines behind one `layer`, for a test to inspect. */
|
|
101
|
+
export declare class ScriptedBrowserbase extends ScriptedBrowserbase_base {}
|
|
102
|
+
export interface ScriptedLayerOptions {
|
|
103
|
+
readonly browser: BrowserTesting.Script;
|
|
104
|
+
readonly provider?: ProviderScript;
|
|
105
|
+
/** Defaults to `recipe()`: a five-minute provider lifetime and a provider-managed viewport. */
|
|
106
|
+
readonly launch?: LaunchRecipe;
|
|
107
|
+
readonly options?: Omit<BrowserOptions, "launch">;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The real account and browser Layers over the scripted provider and the scripted engine.
|
|
111
|
+
* Allocation attempts, release reconciliation, cleanup receipts, `onCleanup`,
|
|
112
|
+
* `onAllocationUncertain`, borrowed attachment and every resource service run for real;
|
|
113
|
+
* only the provider's replies and the browser's pages are scripted. Attempt ids and handoff
|
|
114
|
+
* tokens come from `sequentialCrypto` in `effect-browser/testing`, so they are predictable too.
|
|
115
|
+
*/
|
|
116
|
+
export declare const layer: (options: ScriptedLayerOptions) => Layer.Layer<BrowserbaseBrowser | Services | ScriptedBrowserbase, BrowserError | ClientError>;
|
|
117
|
+
//#endregion
|
|
118
|
+
export {};
|
|
119
|
+
//# sourceMappingURL=Testing.d.mts.map
|
package/dist/Testing.mjs
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { Identifier } from "./References.mjs";
|
|
2
|
+
import { ClientError } from "./Errors.mjs";
|
|
3
|
+
import { recipe } from "./Launch.mjs";
|
|
4
|
+
import { ProviderSession, SessionStatus } from "./SessionData.mjs";
|
|
5
|
+
import { layer as layer$1 } from "./Account.mjs";
|
|
6
|
+
import { layer as layer$2 } from "./BrowserBinding.mjs";
|
|
7
|
+
import { t as BrowserbaseBrowser } from "./Browser-DpSajNai.mjs";
|
|
8
|
+
import { Context, Effect, Layer, Redacted, Schema } from "effect";
|
|
9
|
+
import { FetchHttpClient } from "effect/unstable/http";
|
|
10
|
+
import * as BrowserTesting from "effect-browser/testing";
|
|
11
|
+
//#region src/Testing.ts
|
|
12
|
+
/**
|
|
13
|
+
* How the scripted control plane answers. Everything else about a session is what the real
|
|
14
|
+
* `Client`, `Sessions`, allocation and cleanup code makes of those answers.
|
|
15
|
+
*/
|
|
16
|
+
const ProviderScript = Schema.Struct({
|
|
17
|
+
projectId: Schema.optionalKey(Identifier),
|
|
18
|
+
/**
|
|
19
|
+
* The creation reply: `Accept` allocates a session (running unless told otherwise), `Reject`
|
|
20
|
+
* refuses with a status the real transport classifies, `Malformed` replies without an
|
|
21
|
+
* identity, and `Lost` fails the request after it was sent, so the outcome is unknown.
|
|
22
|
+
*/
|
|
23
|
+
create: Schema.optionalKey(Schema.Union([
|
|
24
|
+
Schema.TaggedStruct("Accept", { status: Schema.optionalKey(SessionStatus) }),
|
|
25
|
+
Schema.TaggedStruct("Reject", {
|
|
26
|
+
status: Schema.Int.check(Schema.isBetween({
|
|
27
|
+
minimum: 400,
|
|
28
|
+
maximum: 599
|
|
29
|
+
})),
|
|
30
|
+
retryAfterMillis: Schema.optionalKey(Schema.Natural)
|
|
31
|
+
}),
|
|
32
|
+
Schema.TaggedStruct("Malformed", {}),
|
|
33
|
+
Schema.TaggedStruct("Lost", {})
|
|
34
|
+
])),
|
|
35
|
+
/** What a release request does: the session ends, stays running, or the request fails. */
|
|
36
|
+
release: Schema.optionalKey(Schema.Literals([
|
|
37
|
+
"confirmed",
|
|
38
|
+
"pending",
|
|
39
|
+
"failed"
|
|
40
|
+
])),
|
|
41
|
+
liveView: Schema.optionalKey(Schema.Literals(["issued", "denied"])),
|
|
42
|
+
region: Schema.optionalKey(ProviderSession.fields.region),
|
|
43
|
+
/**
|
|
44
|
+
* Whose identity a session's retrieval and release replies carry: its own, another session's
|
|
45
|
+
* or another project's. The real client refuses a reply it cannot match, so a test can see
|
|
46
|
+
* what allocation, connection and cleanup make of a provider that answers for someone else.
|
|
47
|
+
*/
|
|
48
|
+
identity: Schema.optionalKey(Schema.Literals([
|
|
49
|
+
"own",
|
|
50
|
+
"foreign-session",
|
|
51
|
+
"foreign-project"
|
|
52
|
+
]))
|
|
53
|
+
});
|
|
54
|
+
const secrets = {
|
|
55
|
+
apiKey: "SCRIPTED-API-KEY-NOT-A-CREDENTIAL",
|
|
56
|
+
connectUrl: "SCRIPTED-CONNECT-KEY",
|
|
57
|
+
liveView: "SCRIPTED-LIVE-VIEW"
|
|
58
|
+
};
|
|
59
|
+
const json = (body, status = 200, headers = {}) => new Response(JSON.stringify(body), {
|
|
60
|
+
status,
|
|
61
|
+
headers: {
|
|
62
|
+
"content-type": "application/json",
|
|
63
|
+
...headers
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* The reviewed session subset of the provider API, answered in memory: creation, retrieval,
|
|
68
|
+
* listing, release, Live View issuance and uploads. Provide it as `FetchHttpClient.Fetch`
|
|
69
|
+
* beneath the real `BrowserbaseClient.layer`, or use `layer` below, which does that.
|
|
70
|
+
*/
|
|
71
|
+
const provider = Effect.fnUntraced(function* (script = {}) {
|
|
72
|
+
const fixed = yield* Schema.decodeEffect(ProviderScript)(script, { onExcessProperty: "error" }).pipe(Effect.mapError(() => ClientError.make({
|
|
73
|
+
operation: "configure",
|
|
74
|
+
reason: "configuration",
|
|
75
|
+
outcome: "undispatched"
|
|
76
|
+
})));
|
|
77
|
+
const projectId = fixed.projectId ?? "project-1";
|
|
78
|
+
const region = fixed.region ?? "us-east-1";
|
|
79
|
+
const calls = [];
|
|
80
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
81
|
+
let serial = 0;
|
|
82
|
+
const identity = fixed.identity ?? "own";
|
|
83
|
+
const row = (session) => ({
|
|
84
|
+
id: identity === "foreign-session" ? `${session.id}-other` : session.id,
|
|
85
|
+
projectId: identity === "foreign-project" ? `${projectId}-other` : projectId,
|
|
86
|
+
status: session.status,
|
|
87
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
88
|
+
updatedAt: "2026-01-01T00:00:01.000Z",
|
|
89
|
+
expiresAt: "2026-01-01T01:00:00.000Z",
|
|
90
|
+
startedAt: "2026-01-01T00:00:01.000Z",
|
|
91
|
+
...session.status === "COMPLETED" || session.status === "ERROR" || session.status === "TIMED_OUT" ? { endedAt: "2026-01-01T00:10:00.000Z" } : {},
|
|
92
|
+
keepAlive: session.keepAlive,
|
|
93
|
+
proxyBytes: 0,
|
|
94
|
+
region,
|
|
95
|
+
connectUrl: `wss://connect.browserbase.com/?session=${session.id}&key=${secrets.connectUrl}`
|
|
96
|
+
});
|
|
97
|
+
const create = fixed.create ?? { _tag: "Accept" };
|
|
98
|
+
const fetch = async (input, init) => {
|
|
99
|
+
const request = new Request(input, init);
|
|
100
|
+
const url = new URL(request.url);
|
|
101
|
+
calls.push({
|
|
102
|
+
method: request.method,
|
|
103
|
+
path: url.pathname
|
|
104
|
+
});
|
|
105
|
+
if (url.origin !== "https://api.browserbase.com") return json({ error: "unexpected origin" }, 404);
|
|
106
|
+
if (request.headers.get("x-bb-api-key") !== secrets.apiKey) return json({ error: "unauthorized" }, 401);
|
|
107
|
+
const parts = url.pathname.split("/").filter((part) => part.length > 0);
|
|
108
|
+
if (parts[0] !== "v1" || parts[1] !== "sessions") return json({}, 404);
|
|
109
|
+
if (parts.length === 2) {
|
|
110
|
+
if (request.method === "GET") return json([...sessions.values()].map(row));
|
|
111
|
+
if (request.method !== "POST") return json({}, 404);
|
|
112
|
+
switch (create._tag) {
|
|
113
|
+
case "Reject": return json({ error: "rejected" }, create.status, create.retryAfterMillis === void 0 ? {} : { "retry-after": String(Math.max(1, Math.ceil(create.retryAfterMillis / 1e3))) });
|
|
114
|
+
case "Malformed": return json({ id: 12 });
|
|
115
|
+
case "Lost": throw new Error("The scripted provider lost this reply");
|
|
116
|
+
case "Accept": {
|
|
117
|
+
const body = await request.json().catch(() => void 0);
|
|
118
|
+
const session = {
|
|
119
|
+
id: `session-${++serial}`,
|
|
120
|
+
keepAlive: typeof body === "object" && body !== null && "keepAlive" in body && body.keepAlive === true,
|
|
121
|
+
status: create.status ?? "RUNNING",
|
|
122
|
+
releaseRequests: 0,
|
|
123
|
+
uploads: []
|
|
124
|
+
};
|
|
125
|
+
sessions.set(session.id, session);
|
|
126
|
+
return json({
|
|
127
|
+
id: session.id,
|
|
128
|
+
projectId
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const session = sessions.get(parts[2] ?? "");
|
|
134
|
+
if (session === void 0) return json({ error: "not found" }, 404);
|
|
135
|
+
if (parts.length === 3) {
|
|
136
|
+
if (request.method === "GET") return json(row(session));
|
|
137
|
+
if (request.method !== "POST") return json({}, 404);
|
|
138
|
+
session.releaseRequests++;
|
|
139
|
+
switch (fixed.release ?? "confirmed") {
|
|
140
|
+
case "confirmed":
|
|
141
|
+
session.status = "COMPLETED";
|
|
142
|
+
return json(row(session));
|
|
143
|
+
case "pending": return json(row(session));
|
|
144
|
+
case "failed": return json({ error: "release failed" }, 500);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (parts[3] === "debug" && request.method === "GET") return fixed.liveView === "denied" ? json({ error: "denied" }, 403) : json({
|
|
148
|
+
debuggerFullscreenUrl: `https://www.browserbase.com/devtools-fullscreen/inspector.html?scripted=${secrets.liveView}`,
|
|
149
|
+
pages: []
|
|
150
|
+
});
|
|
151
|
+
if (parts[3] === "uploads" && request.method === "POST") {
|
|
152
|
+
const file = (await request.formData()).get("file");
|
|
153
|
+
session.uploads.push(file instanceof File ? file.name : "upload");
|
|
154
|
+
return json({ message: "File uploaded successfully" });
|
|
155
|
+
}
|
|
156
|
+
return json({}, 404);
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
fetch,
|
|
160
|
+
control: {
|
|
161
|
+
projectId,
|
|
162
|
+
calls: Effect.sync(() => calls.map((call) => Object.freeze({ ...call }))),
|
|
163
|
+
sessions: Effect.sync(() => [...sessions.values()].map((session) => Object.freeze({
|
|
164
|
+
id: session.id,
|
|
165
|
+
status: session.status,
|
|
166
|
+
releaseRequests: session.releaseRequests,
|
|
167
|
+
uploads: Object.freeze([...session.uploads])
|
|
168
|
+
}))),
|
|
169
|
+
setStatus: (sessionId, status) => Effect.sync(() => {
|
|
170
|
+
const session = sessions.get(sessionId);
|
|
171
|
+
if (session !== void 0) session.status = status;
|
|
172
|
+
}),
|
|
173
|
+
secrets
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
/** The scripted provider and the scripted engines behind one `layer`, for a test to inspect. */
|
|
178
|
+
var ScriptedBrowserbase = class extends Context.Service()("effect-browserbase/testing/ScriptedBrowserbase") {};
|
|
179
|
+
/**
|
|
180
|
+
* The real account and browser Layers over the scripted provider and the scripted engine.
|
|
181
|
+
* Allocation attempts, release reconciliation, cleanup receipts, `onCleanup`,
|
|
182
|
+
* `onAllocationUncertain`, borrowed attachment and every resource service run for real;
|
|
183
|
+
* only the provider's replies and the browser's pages are scripted. Attempt ids and handoff
|
|
184
|
+
* tokens come from `sequentialCrypto` in `effect-browser/testing`, so they are predictable too.
|
|
185
|
+
*/
|
|
186
|
+
const layer = (options) => Layer.unwrap(Effect.gen(function* () {
|
|
187
|
+
const scripted = yield* provider(options.provider);
|
|
188
|
+
const engine = yield* BrowserTesting.binding(options.browser);
|
|
189
|
+
const account = layer$1({
|
|
190
|
+
projectId: scripted.control.projectId,
|
|
191
|
+
apiKey: Redacted.make(scripted.control.secrets.apiKey)
|
|
192
|
+
}).pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, scripted.fetch)));
|
|
193
|
+
const browser = BrowserbaseBrowser.layer({
|
|
194
|
+
launch: options.launch ?? recipe(),
|
|
195
|
+
...options.options
|
|
196
|
+
}).pipe(Layer.provide(account), Layer.provide(layer$2(engine.binding)), Layer.provide(BrowserTesting.sequentialCrypto));
|
|
197
|
+
const control = Layer.succeed(ScriptedBrowserbase, ScriptedBrowserbase.of({
|
|
198
|
+
provider: scripted.control,
|
|
199
|
+
browsers: engine.browsers
|
|
200
|
+
}));
|
|
201
|
+
return Layer.mergeAll(browser, account, control);
|
|
202
|
+
}));
|
|
203
|
+
//#endregion
|
|
204
|
+
export { ProviderScript, ScriptedBrowserbase, layer, provider };
|
|
205
|
+
|
|
206
|
+
//# sourceMappingURL=Testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Testing.mjs","names":["accountLayer","bindingLayer"],"sources":["../src/Testing.ts"],"sourcesContent":["import { Context, Effect, Layer, Redacted, Schema } from \"effect\";\nimport type { BrowserError } from \"effect-browser/errors\";\nimport * as BrowserTesting from \"effect-browser/testing\";\nimport { FetchHttpClient } from \"effect/unstable/http\";\n\nimport { type Services as AccountServices, layer as accountLayer } from \"./Account.ts\";\nimport { BrowserbaseBrowser, type BrowserOptions } from \"./Browser.ts\";\nimport { layer as bindingLayer } from \"./BrowserBinding.ts\";\nimport { ClientError } from \"./Errors.ts\";\nimport { type LaunchRecipe, recipe } from \"./Launch.ts\";\nimport { Identifier } from \"./References.ts\";\nimport { ProviderSession, SessionStatus } from \"./SessionData.ts\";\n\n/**\n * How the scripted control plane answers. Everything else about a session is what the real\n * `Client`, `Sessions`, allocation and cleanup code makes of those answers.\n */\nexport const ProviderScript = Schema.Struct({\n projectId: Schema.optionalKey(Identifier),\n /**\n * The creation reply: `Accept` allocates a session (running unless told otherwise), `Reject`\n * refuses with a status the real transport classifies, `Malformed` replies without an\n * identity, and `Lost` fails the request after it was sent, so the outcome is unknown.\n */\n create: Schema.optionalKey(\n Schema.Union([\n Schema.TaggedStruct(\"Accept\", { status: Schema.optionalKey(SessionStatus) }),\n Schema.TaggedStruct(\"Reject\", {\n status: Schema.Int.check(Schema.isBetween({ minimum: 400, maximum: 599 })),\n retryAfterMillis: Schema.optionalKey(Schema.Natural),\n }),\n Schema.TaggedStruct(\"Malformed\", {}),\n Schema.TaggedStruct(\"Lost\", {}),\n ]),\n ),\n /** What a release request does: the session ends, stays running, or the request fails. */\n release: Schema.optionalKey(Schema.Literals([\"confirmed\", \"pending\", \"failed\"])),\n liveView: Schema.optionalKey(Schema.Literals([\"issued\", \"denied\"])),\n region: Schema.optionalKey(ProviderSession.fields.region),\n /**\n * Whose identity a session's retrieval and release replies carry: its own, another session's\n * or another project's. The real client refuses a reply it cannot match, so a test can see\n * what allocation, connection and cleanup make of a provider that answers for someone else.\n */\n identity: Schema.optionalKey(Schema.Literals([\"own\", \"foreign-session\", \"foreign-project\"])),\n});\n\nexport type ProviderScript = typeof ProviderScript.Type;\n\nexport interface ProviderCall {\n readonly method: string;\n readonly path: string;\n}\n\nexport interface ProviderSessionRecord {\n readonly id: string;\n readonly status: SessionStatus;\n readonly releaseRequests: number;\n readonly uploads: ReadonlyArray<string>;\n}\n\nexport interface ProviderControl {\n readonly projectId: string;\n readonly calls: Effect.Effect<ReadonlyArray<ProviderCall>>;\n readonly sessions: Effect.Effect<ReadonlyArray<ProviderSessionRecord>>;\n /** Move a session as the provider might while it is connected, for example to `TIMED_OUT`. */\n readonly setStatus: (sessionId: string, status: SessionStatus) => Effect.Effect<void>;\n /** Marker strings the double uses where real secrets would be, to grep evidence for leaks. */\n readonly secrets: {\n readonly apiKey: string;\n readonly connectUrl: string;\n readonly liveView: string;\n };\n}\n\n/** A scripted control plane: a `FetchHttpClient.Fetch` and the handle that inspects it. */\nexport interface ScriptedProvider {\n readonly fetch: typeof globalThis.fetch;\n readonly control: ProviderControl;\n}\n\nconst secrets = {\n apiKey: \"SCRIPTED-API-KEY-NOT-A-CREDENTIAL\",\n connectUrl: \"SCRIPTED-CONNECT-KEY\",\n liveView: \"SCRIPTED-LIVE-VIEW\",\n} as const;\n\ninterface SessionRow {\n readonly id: string;\n readonly keepAlive: boolean;\n status: SessionStatus;\n releaseRequests: number;\n readonly uploads: Array<string>;\n}\n\nconst json = (body: unknown, status = 200, headers: Record<string, string> = {}) =>\n new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\", ...headers },\n });\n\n/**\n * The reviewed session subset of the provider API, answered in memory: creation, retrieval,\n * listing, release, Live View issuance and uploads. Provide it as `FetchHttpClient.Fetch`\n * beneath the real `BrowserbaseClient.layer`, or use `layer` below, which does that.\n */\nexport const provider = Effect.fnUntraced(function* (\n script: ProviderScript = {},\n): Effect.fn.Return<ScriptedProvider, ClientError> {\n const fixed = yield* Schema.decodeEffect(ProviderScript)(script, {\n onExcessProperty: \"error\",\n }).pipe(\n Effect.mapError(() =>\n ClientError.make({\n operation: \"configure\",\n reason: \"configuration\",\n outcome: \"undispatched\",\n }),\n ),\n );\n\n const projectId = fixed.projectId ?? \"project-1\";\n const region = fixed.region ?? \"us-east-1\";\n const calls: Array<ProviderCall> = [];\n const sessions = new Map<string, SessionRow>();\n let serial = 0;\n\n const identity = fixed.identity ?? \"own\";\n\n const row = (session: SessionRow) => ({\n id: identity === \"foreign-session\" ? `${session.id}-other` : session.id,\n projectId: identity === \"foreign-project\" ? `${projectId}-other` : projectId,\n status: session.status,\n createdAt: \"2026-01-01T00:00:00.000Z\",\n updatedAt: \"2026-01-01T00:00:01.000Z\",\n expiresAt: \"2026-01-01T01:00:00.000Z\",\n startedAt: \"2026-01-01T00:00:01.000Z\",\n ...(session.status === \"COMPLETED\" ||\n session.status === \"ERROR\" ||\n session.status === \"TIMED_OUT\"\n ? { endedAt: \"2026-01-01T00:10:00.000Z\" }\n : {}),\n keepAlive: session.keepAlive,\n proxyBytes: 0,\n region,\n connectUrl: `wss://connect.browserbase.com/?session=${session.id}&key=${secrets.connectUrl}`,\n });\n\n const create = fixed.create ?? { _tag: \"Accept\" as const };\n\n // oxlint-disable-next-line effecttsgo/async-function -- stands in for the platform's fetch\n const fetch: typeof globalThis.fetch = async (input, init) => {\n const request = new Request(input, init);\n const url = new URL(request.url);\n\n calls.push({ method: request.method, path: url.pathname });\n if (url.origin !== \"https://api.browserbase.com\")\n return json({ error: \"unexpected origin\" }, 404);\n if (request.headers.get(\"x-bb-api-key\") !== secrets.apiKey)\n return json({ error: \"unauthorized\" }, 401);\n const parts = url.pathname.split(\"/\").filter((part) => part.length > 0);\n\n if (parts[0] !== \"v1\" || parts[1] !== \"sessions\") return json({}, 404);\n if (parts.length === 2) {\n if (request.method === \"GET\") return json([...sessions.values()].map(row));\n if (request.method !== \"POST\") return json({}, 404);\n switch (create._tag) {\n case \"Reject\":\n return json(\n { error: \"rejected\" },\n create.status,\n create.retryAfterMillis === undefined\n ? {}\n : { \"retry-after\": String(Math.max(1, Math.ceil(create.retryAfterMillis / 1000))) },\n );\n case \"Malformed\":\n return json({ id: 12 });\n case \"Lost\":\n throw new Error(\"The scripted provider lost this reply\");\n case \"Accept\": {\n const body: unknown = await request.json().catch(() => undefined);\n\n const session: SessionRow = {\n id: `session-${++serial}`,\n keepAlive:\n typeof body === \"object\" &&\n body !== null &&\n \"keepAlive\" in body &&\n body.keepAlive === true,\n status: create.status ?? \"RUNNING\",\n releaseRequests: 0,\n uploads: [],\n };\n\n sessions.set(session.id, session);\n\n return json({ id: session.id, projectId });\n }\n }\n }\n const session = sessions.get(parts[2] ?? \"\");\n\n if (session === undefined) return json({ error: \"not found\" }, 404);\n if (parts.length === 3) {\n if (request.method === \"GET\") return json(row(session));\n if (request.method !== \"POST\") return json({}, 404);\n session.releaseRequests++;\n switch (fixed.release ?? \"confirmed\") {\n case \"confirmed\":\n session.status = \"COMPLETED\";\n\n return json(row(session));\n case \"pending\":\n return json(row(session));\n case \"failed\":\n return json({ error: \"release failed\" }, 500);\n }\n }\n if (parts[3] === \"debug\" && request.method === \"GET\")\n return fixed.liveView === \"denied\"\n ? json({ error: \"denied\" }, 403)\n : json({\n debuggerFullscreenUrl: `https://www.browserbase.com/devtools-fullscreen/inspector.html?scripted=${secrets.liveView}`,\n pages: [],\n });\n if (parts[3] === \"uploads\" && request.method === \"POST\") {\n const file = (await request.formData()).get(\"file\");\n\n session.uploads.push(file instanceof File ? file.name : \"upload\");\n\n return json({ message: \"File uploaded successfully\" });\n }\n\n return json({}, 404);\n };\n\n const control: ProviderControl = {\n projectId,\n calls: Effect.sync(() => calls.map((call) => Object.freeze({ ...call }))),\n sessions: Effect.sync(() =>\n [...sessions.values()].map((session) =>\n Object.freeze({\n id: session.id,\n status: session.status,\n releaseRequests: session.releaseRequests,\n uploads: Object.freeze([...session.uploads]),\n }),\n ),\n ),\n setStatus: (sessionId, status) =>\n Effect.sync(() => {\n const session = sessions.get(sessionId);\n\n if (session !== undefined) session.status = status;\n }),\n secrets,\n };\n\n return { fetch, control };\n});\n\n/** The scripted provider and the scripted engines behind one `layer`, for a test to inspect. */\nexport class ScriptedBrowserbase extends Context.Service<\n ScriptedBrowserbase,\n {\n readonly provider: ProviderControl;\n /**\n * One control for each scripted browser the Layer connected to, in first-connection order:\n * one per provider session, which a keep-alive reconnection or a borrowed attachment reaches\n * again.\n */\n readonly browsers: Effect.Effect<ReadonlyArray<BrowserTesting.ScriptedControl>>;\n }\n>()(\"effect-browserbase/testing/ScriptedBrowserbase\") {}\n\nexport interface ScriptedLayerOptions {\n readonly browser: BrowserTesting.Script;\n readonly provider?: ProviderScript;\n /** Defaults to `recipe()`: a five-minute provider lifetime and a provider-managed viewport. */\n readonly launch?: LaunchRecipe;\n readonly options?: Omit<BrowserOptions, \"launch\">;\n}\n\n/**\n * The real account and browser Layers over the scripted provider and the scripted engine.\n * Allocation attempts, release reconciliation, cleanup receipts, `onCleanup`,\n * `onAllocationUncertain`, borrowed attachment and every resource service run for real;\n * only the provider's replies and the browser's pages are scripted. Attempt ids and handoff\n * tokens come from `sequentialCrypto` in `effect-browser/testing`, so they are predictable too.\n */\nexport const layer = (\n options: ScriptedLayerOptions,\n): Layer.Layer<\n BrowserbaseBrowser | AccountServices | ScriptedBrowserbase,\n BrowserError | ClientError\n> =>\n Layer.unwrap(\n Effect.gen(function* () {\n const scripted = yield* provider(options.provider);\n const engine = yield* BrowserTesting.binding(options.browser);\n\n const account = accountLayer({\n projectId: scripted.control.projectId,\n apiKey: Redacted.make(scripted.control.secrets.apiKey),\n }).pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, scripted.fetch)));\n\n const browser = BrowserbaseBrowser.layer({\n launch: options.launch ?? recipe(),\n ...options.options,\n }).pipe(\n Layer.provide(account),\n Layer.provide(bindingLayer(engine.binding)),\n Layer.provide(BrowserTesting.sequentialCrypto),\n );\n\n const control = Layer.succeed(\n ScriptedBrowserbase,\n ScriptedBrowserbase.of({ provider: scripted.control, browsers: engine.browsers }),\n );\n\n return Layer.mergeAll(browser, account, control);\n }),\n );\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { a as SessionReference } from "./References-DOH3Wu44.mjs";
|
|
2
|
+
import { FileError } from "./Errors.mjs";
|
|
3
|
+
import { t as BrowserbaseClient } from "./Client-B_f6bBLT.mjs";
|
|
4
|
+
import { t as BrowserbaseSessions } from "./Sessions-BpZgjteE.mjs";
|
|
5
|
+
import { p as UploadReceipt } from "./Transfers-GVqzrDWl.mjs";
|
|
6
|
+
import { Context, Effect, Layer } from "effect";
|
|
7
|
+
//#region src/Uploads.d.ts
|
|
8
|
+
interface UploadFile {
|
|
9
|
+
readonly filename: string;
|
|
10
|
+
readonly mediaType: string;
|
|
11
|
+
readonly bytes: Uint8Array;
|
|
12
|
+
readonly maxBytes?: number;
|
|
13
|
+
readonly timeoutMillis?: number;
|
|
14
|
+
}
|
|
15
|
+
declare const BrowserbaseUploads_base: Context.ServiceClass<BrowserbaseUploads, "effect-browserbase/Uploads", {
|
|
16
|
+
readonly create: (reference: SessionReference, file: UploadFile) => Effect.Effect<UploadReceipt, FileError>;
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* A session upload places bytes where the remote browser can already reach them. It is the
|
|
20
|
+
* large-file half of file selection; small in-memory selection needs no provisioning at all.
|
|
21
|
+
*/
|
|
22
|
+
declare class BrowserbaseUploads extends BrowserbaseUploads_base {
|
|
23
|
+
static readonly layer: Layer.Layer<BrowserbaseUploads, never, BrowserbaseClient | BrowserbaseSessions>;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { UploadFile as n, BrowserbaseUploads as t };
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=Uploads-C2neBEpj.d.mts.map
|
package/dist/Uploads.d.mts
CHANGED
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { t as BrowserbaseSessions } from "./Sessions-tFJYaXFq.mjs";
|
|
5
|
-
import { p as UploadReceipt } from "./Transfers-GVqzrDWl.mjs";
|
|
6
|
-
import { Context, Effect, Layer } from "effect";
|
|
7
|
-
//#region src/Uploads.d.ts
|
|
8
|
-
export interface UploadFile {
|
|
9
|
-
readonly filename: string;
|
|
10
|
-
readonly mediaType: string;
|
|
11
|
-
readonly bytes: Uint8Array;
|
|
12
|
-
readonly maxBytes?: number;
|
|
13
|
-
readonly timeoutMillis?: number;
|
|
14
|
-
}
|
|
15
|
-
declare const BrowserbaseUploads_base: Context.ServiceClass<BrowserbaseUploads, "effect-browserbase/Uploads", {
|
|
16
|
-
readonly create: (reference: SessionReference, file: UploadFile) => Effect.Effect<UploadReceipt, FileError>;
|
|
17
|
-
}>;
|
|
18
|
-
/**
|
|
19
|
-
* A session upload places bytes where the remote browser can already reach them. It is the
|
|
20
|
-
* large-file half of file selection; small in-memory selection needs no provisioning at all.
|
|
21
|
-
*/
|
|
22
|
-
export declare class BrowserbaseUploads extends BrowserbaseUploads_base {
|
|
23
|
-
static readonly layer: Layer.Layer<BrowserbaseUploads, never, BrowserbaseClient | BrowserbaseSessions>;
|
|
24
|
-
}
|
|
25
|
-
//#endregion
|
|
26
|
-
export {};
|
|
27
|
-
//# sourceMappingURL=Uploads.d.mts.map
|
|
1
|
+
import { n as UploadFile, t as BrowserbaseUploads } from "./Uploads-C2neBEpj.mjs";
|
|
2
|
+
export { BrowserbaseUploads, UploadFile };
|
|
3
|
+
export {};
|
package/dist/Uploads.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FileError } from "./Errors.mjs";
|
|
2
2
|
import { RemoteFilePath, SafeFilename, UploadReceipt } from "./Transfers.mjs";
|
|
3
|
-
import { t as BrowserbaseClient } from "./Client-
|
|
4
|
-
import { t as BrowserbaseSessions } from "./Sessions-
|
|
3
|
+
import { t as BrowserbaseClient } from "./Client-4RMKIpL2.mjs";
|
|
4
|
+
import { t as BrowserbaseSessions } from "./Sessions-suOLSym9.mjs";
|
|
5
5
|
import { n as recordIssuedUpload } from "./Issued-DejqoMYL.mjs";
|
|
6
6
|
import { Context, Effect, Layer, Schema } from "effect";
|
|
7
7
|
//#region src/Uploads.ts
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PlatformError } from "./Errors.mjs";
|
|
2
|
+
import { t as BrowserbaseClient } from "./Client-B_f6bBLT.mjs";
|
|
3
|
+
import { Context, Effect, Layer, Redacted, Schema } from "effect";
|
|
4
|
+
//#region src/Webhooks.d.ts
|
|
5
|
+
/** Provider event names. Browserbase currently emits Functions events only. */
|
|
6
|
+
declare const WebhookEventType: Schema.Literals<readonly ["functions.builds.running", "functions.builds.completed", "functions.builds.failed", "functions.invocations.pending", "functions.invocations.running", "functions.invocations.completed", "functions.invocations.failed"]>;
|
|
7
|
+
type WebhookEventType = typeof WebhookEventType.Type;
|
|
8
|
+
declare const WebhookSettings: Schema.Struct<{
|
|
9
|
+
readonly endpoint: Schema.NonEmptyString;
|
|
10
|
+
readonly eventTypes: Schema.$Array<Schema.Literals<readonly ["functions.builds.running", "functions.builds.completed", "functions.builds.failed", "functions.invocations.pending", "functions.invocations.running", "functions.invocations.completed", "functions.invocations.failed"]>>;
|
|
11
|
+
}>;
|
|
12
|
+
type WebhookSettings = typeof WebhookSettings.Type;
|
|
13
|
+
declare const WebhookUpdate: Schema.Struct<{
|
|
14
|
+
readonly endpoint: Schema.optionalKey<Schema.NonEmptyString>;
|
|
15
|
+
readonly eventTypes: Schema.optionalKey<Schema.$Array<Schema.Literals<readonly ["functions.builds.running", "functions.builds.completed", "functions.builds.failed", "functions.invocations.pending", "functions.invocations.running", "functions.invocations.completed", "functions.invocations.failed"]>>>;
|
|
16
|
+
}>;
|
|
17
|
+
type WebhookUpdate = typeof WebhookUpdate.Type;
|
|
18
|
+
declare const WebhookMetadata_base: Schema.Class<WebhookMetadata, Schema.Struct<{
|
|
19
|
+
readonly webhookId: Schema.NonEmptyString;
|
|
20
|
+
readonly projectId: Schema.NonEmptyString;
|
|
21
|
+
readonly endpoint: Schema.String;
|
|
22
|
+
readonly eventTypes: Schema.$Array<Schema.String>;
|
|
23
|
+
readonly createdAt: Schema.String;
|
|
24
|
+
readonly updatedAt: Schema.String;
|
|
25
|
+
}>, {}>;
|
|
26
|
+
declare class WebhookMetadata extends WebhookMetadata_base {}
|
|
27
|
+
/** A signing secret is returned once, redacted so it cannot be logged by accident. */
|
|
28
|
+
interface WebhookRegistration {
|
|
29
|
+
readonly webhook: WebhookMetadata;
|
|
30
|
+
readonly secret: Redacted.Redacted<string>;
|
|
31
|
+
}
|
|
32
|
+
interface WebhookPage {
|
|
33
|
+
readonly webhooks: ReadonlyArray<WebhookMetadata>;
|
|
34
|
+
readonly nextCursor: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
declare const WebhookListQuery: Schema.Struct<{
|
|
37
|
+
readonly cursor: Schema.optionalKey<Schema.NonEmptyString>;
|
|
38
|
+
readonly limit: Schema.optionalKey<Schema.Int>;
|
|
39
|
+
}>;
|
|
40
|
+
type WebhookListQuery = typeof WebhookListQuery.Type;
|
|
41
|
+
declare const BrowserbaseWebhooks_base: Context.ServiceClass<BrowserbaseWebhooks, "effect-browserbase/Webhooks", {
|
|
42
|
+
readonly create: (settings: WebhookSettings) => Effect.Effect<WebhookRegistration, PlatformError>;
|
|
43
|
+
readonly list: (query?: WebhookListQuery) => Effect.Effect<WebhookPage, PlatformError>;
|
|
44
|
+
readonly retrieve: (webhookId: string) => Effect.Effect<WebhookMetadata, PlatformError>;
|
|
45
|
+
readonly update: (webhookId: string, update: WebhookUpdate) => Effect.Effect<WebhookMetadata, PlatformError>;
|
|
46
|
+
readonly delete: (webhookId: string) => Effect.Effect<void, PlatformError>;
|
|
47
|
+
/** Issues a new secret; the old one keeps working unless `revokeImmediately`. */
|
|
48
|
+
readonly rotateSecret: (webhookId: string, options?: {
|
|
49
|
+
readonly revokeImmediately?: boolean;
|
|
50
|
+
}) => Effect.Effect<Redacted.Redacted<string>, PlatformError>;
|
|
51
|
+
}>;
|
|
52
|
+
/** Webhook administration for the Client's project. Mutations are never retried. */
|
|
53
|
+
declare class BrowserbaseWebhooks extends BrowserbaseWebhooks_base {
|
|
54
|
+
static readonly layer: Layer.Layer<BrowserbaseWebhooks, never, BrowserbaseClient>;
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { WebhookPage as a, WebhookUpdate as c, WebhookMetadata as i, WebhookEventType as n, WebhookRegistration as o, WebhookListQuery as r, WebhookSettings as s, BrowserbaseWebhooks as t };
|
|
58
|
+
export {};
|
|
59
|
+
//# sourceMappingURL=Webhooks-BoeRAxzG.d.mts.map
|
package/dist/Webhooks.d.mts
CHANGED
|
@@ -1,58 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
//#region src/Webhooks.d.ts
|
|
5
|
-
/** Provider event names. Browserbase currently emits Functions events only. */
|
|
6
|
-
export declare const WebhookEventType: Schema.Literals<readonly ["functions.builds.running", "functions.builds.completed", "functions.builds.failed", "functions.invocations.pending", "functions.invocations.running", "functions.invocations.completed", "functions.invocations.failed"]>;
|
|
7
|
-
export type WebhookEventType = typeof WebhookEventType.Type;
|
|
8
|
-
export declare const WebhookSettings: Schema.Struct<{
|
|
9
|
-
readonly endpoint: Schema.NonEmptyString;
|
|
10
|
-
readonly eventTypes: Schema.$Array<Schema.Literals<readonly ["functions.builds.running", "functions.builds.completed", "functions.builds.failed", "functions.invocations.pending", "functions.invocations.running", "functions.invocations.completed", "functions.invocations.failed"]>>;
|
|
11
|
-
}>;
|
|
12
|
-
export type WebhookSettings = typeof WebhookSettings.Type;
|
|
13
|
-
export declare const WebhookUpdate: Schema.Struct<{
|
|
14
|
-
readonly endpoint: Schema.optionalKey<Schema.NonEmptyString>;
|
|
15
|
-
readonly eventTypes: Schema.optionalKey<Schema.$Array<Schema.Literals<readonly ["functions.builds.running", "functions.builds.completed", "functions.builds.failed", "functions.invocations.pending", "functions.invocations.running", "functions.invocations.completed", "functions.invocations.failed"]>>>;
|
|
16
|
-
}>;
|
|
17
|
-
export type WebhookUpdate = typeof WebhookUpdate.Type;
|
|
18
|
-
declare const WebhookMetadata_base: Schema.Class<WebhookMetadata, Schema.Struct<{
|
|
19
|
-
readonly webhookId: Schema.NonEmptyString;
|
|
20
|
-
readonly projectId: Schema.NonEmptyString;
|
|
21
|
-
readonly endpoint: Schema.String;
|
|
22
|
-
readonly eventTypes: Schema.$Array<Schema.String>;
|
|
23
|
-
readonly createdAt: Schema.String;
|
|
24
|
-
readonly updatedAt: Schema.String;
|
|
25
|
-
}>, {}>;
|
|
26
|
-
export declare class WebhookMetadata extends WebhookMetadata_base {}
|
|
27
|
-
/** A signing secret is returned once, redacted so it cannot be logged by accident. */
|
|
28
|
-
export interface WebhookRegistration {
|
|
29
|
-
readonly webhook: WebhookMetadata;
|
|
30
|
-
readonly secret: Redacted.Redacted<string>;
|
|
31
|
-
}
|
|
32
|
-
export interface WebhookPage {
|
|
33
|
-
readonly webhooks: ReadonlyArray<WebhookMetadata>;
|
|
34
|
-
readonly nextCursor: string | undefined;
|
|
35
|
-
}
|
|
36
|
-
export declare const WebhookListQuery: Schema.Struct<{
|
|
37
|
-
readonly cursor: Schema.optionalKey<Schema.NonEmptyString>;
|
|
38
|
-
readonly limit: Schema.optionalKey<Schema.Int>;
|
|
39
|
-
}>;
|
|
40
|
-
export type WebhookListQuery = typeof WebhookListQuery.Type;
|
|
41
|
-
declare const BrowserbaseWebhooks_base: Context.ServiceClass<BrowserbaseWebhooks, "effect-browserbase/Webhooks", {
|
|
42
|
-
readonly create: (settings: WebhookSettings) => Effect.Effect<WebhookRegistration, PlatformError>;
|
|
43
|
-
readonly list: (query?: WebhookListQuery) => Effect.Effect<WebhookPage, PlatformError>;
|
|
44
|
-
readonly retrieve: (webhookId: string) => Effect.Effect<WebhookMetadata, PlatformError>;
|
|
45
|
-
readonly update: (webhookId: string, update: WebhookUpdate) => Effect.Effect<WebhookMetadata, PlatformError>;
|
|
46
|
-
readonly delete: (webhookId: string) => Effect.Effect<void, PlatformError>;
|
|
47
|
-
/** Issues a new secret; the old one keeps working unless `revokeImmediately`. */
|
|
48
|
-
readonly rotateSecret: (webhookId: string, options?: {
|
|
49
|
-
readonly revokeImmediately?: boolean;
|
|
50
|
-
}) => Effect.Effect<Redacted.Redacted<string>, PlatformError>;
|
|
51
|
-
}>;
|
|
52
|
-
/** Webhook administration for the Client's project. Mutations are never retried. */
|
|
53
|
-
export declare class BrowserbaseWebhooks extends BrowserbaseWebhooks_base {
|
|
54
|
-
static readonly layer: Layer.Layer<BrowserbaseWebhooks, never, BrowserbaseClient>;
|
|
55
|
-
}
|
|
56
|
-
//#endregion
|
|
57
|
-
export {};
|
|
58
|
-
//# sourceMappingURL=Webhooks.d.mts.map
|
|
1
|
+
import { a as WebhookPage, c as WebhookUpdate, i as WebhookMetadata, n as WebhookEventType, o as WebhookRegistration, r as WebhookListQuery, s as WebhookSettings, t as BrowserbaseWebhooks } from "./Webhooks-BoeRAxzG.mjs";
|
|
2
|
+
export { BrowserbaseWebhooks, WebhookEventType, WebhookListQuery, WebhookMetadata, WebhookPage, WebhookRegistration, WebhookSettings, WebhookUpdate };
|
|
3
|
+
export {};
|
package/dist/Webhooks.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Identifier } from "./References.mjs";
|
|
2
2
|
import { PlatformError } from "./Errors.mjs";
|
|
3
|
-
import { t as BrowserbaseClient } from "./Client-
|
|
3
|
+
import { t as BrowserbaseClient } from "./Client-4RMKIpL2.mjs";
|
|
4
4
|
import { t as resource } from "./Resource-DBksC4zB.mjs";
|
|
5
5
|
import { Context, Effect, Layer, Redacted, Schema } from "effect";
|
|
6
6
|
//#region src/Webhooks.ts
|
|
@@ -7,13 +7,9 @@ import { Effect, Schema } from "effect";
|
|
|
7
7
|
* implementation need. Keeping it here leaves one direction of dependency:
|
|
8
8
|
* `ContextCoordination` and `ContextWriter` both read this module, never each other.
|
|
9
9
|
*/
|
|
10
|
-
declare const PersistenceEvidence: Schema.Union<readonly [Schema.
|
|
11
|
-
readonly _tag: Schema.Literal<"NotRequested">;
|
|
12
|
-
}>, Schema.Struct<{
|
|
13
|
-
readonly _tag: Schema.Literal<"Unconfirmed">;
|
|
10
|
+
declare const PersistenceEvidence: Schema.Union<readonly [Schema.TaggedStruct<"NotRequested", {}>, Schema.TaggedStruct<"Unconfirmed", {
|
|
14
11
|
readonly reason: Schema.Literals<readonly ["writer-active", "writer-unknown", "flush-unacknowledged"]>;
|
|
15
|
-
}>, Schema.
|
|
16
|
-
readonly _tag: Schema.Literal<"Observed">;
|
|
12
|
+
}>, Schema.TaggedStruct<"Observed", {
|
|
17
13
|
readonly method: Schema.Literal<"consumer-readback">;
|
|
18
14
|
readonly observedAtMillis: Schema.Finite;
|
|
19
15
|
}>]>;
|
|
@@ -28,13 +24,9 @@ declare class WriterAttemptFacts extends WriterAttemptFacts_base {}
|
|
|
28
24
|
declare const WriterSettlementFacts_base: Schema.Class<WriterSettlementFacts, Schema.Struct<{
|
|
29
25
|
readonly reference: typeof ContextReference;
|
|
30
26
|
readonly attempts: Schema.$Array<typeof WriterAttemptFacts>;
|
|
31
|
-
readonly persistence: Schema.Union<readonly [Schema.
|
|
32
|
-
readonly _tag: Schema.Literal<"NotRequested">;
|
|
33
|
-
}>, Schema.Struct<{
|
|
34
|
-
readonly _tag: Schema.Literal<"Unconfirmed">;
|
|
27
|
+
readonly persistence: Schema.Union<readonly [Schema.TaggedStruct<"NotRequested", {}>, Schema.TaggedStruct<"Unconfirmed", {
|
|
35
28
|
readonly reason: Schema.Literals<readonly ["writer-active", "writer-unknown", "flush-unacknowledged"]>;
|
|
36
|
-
}>, Schema.
|
|
37
|
-
readonly _tag: Schema.Literal<"Observed">;
|
|
29
|
+
}>, Schema.TaggedStruct<"Observed", {
|
|
38
30
|
readonly method: Schema.Literal<"consumer-readback">;
|
|
39
31
|
readonly observedAtMillis: Schema.Finite;
|
|
40
32
|
}>]>;
|
|
@@ -59,4 +51,4 @@ interface WriterOptions<A, VerifyE, VerifyR> {
|
|
|
59
51
|
//#endregion
|
|
60
52
|
export { WriterLease as a, WriterAttemptFacts as i, ContextWriterPermit as n, WriterOptions as o, PersistenceEvidence as r, WriterSettlementFacts as s, ContextWriterBackend as t };
|
|
61
53
|
export {};
|
|
62
|
-
//# sourceMappingURL=WriterFacts-
|
|
54
|
+
//# sourceMappingURL=WriterFacts-BBJRDZYY.d.mts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-browserbase",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.1",
|
|
4
4
|
"description": "Effect-based Browserbase resources and hosted integration for effect-browser.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
17
|
"effect": "^4.0.0-rc.115",
|
|
18
|
-
"effect-browser": "0.2.0-beta.
|
|
18
|
+
"effect-browser": "0.2.0-beta.1"
|
|
19
19
|
},
|
|
20
20
|
"homepage": "https://github.com/mannyc2/effect-agent-browserbase#readme",
|
|
21
21
|
"bugs": {
|
|
@@ -121,6 +121,10 @@
|
|
|
121
121
|
"types": "./dist/Sessions.d.mts",
|
|
122
122
|
"default": "./dist/Sessions.mjs"
|
|
123
123
|
},
|
|
124
|
+
"./testing": {
|
|
125
|
+
"types": "./dist/Testing.d.mts",
|
|
126
|
+
"default": "./dist/Testing.mjs"
|
|
127
|
+
},
|
|
124
128
|
"./transfers": {
|
|
125
129
|
"types": "./dist/Transfers.d.mts",
|
|
126
130
|
"default": "./dist/Transfers.mjs"
|