@hraness/direct 0.7.5

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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +436 -0
  3. package/dist/core/index.js +162 -0
  4. package/dist/index-1csg00w4.js +1167 -0
  5. package/dist/index-6mdfd2ey.js +464 -0
  6. package/dist/index-7n1h75n6.js +616 -0
  7. package/dist/index.js +232 -0
  8. package/dist/react.js +32 -0
  9. package/dist/testing/index.js +1069 -0
  10. package/dist/tooling/bombadil.js +2117 -0
  11. package/dist/tooling/browser-verification-entry.js +1499 -0
  12. package/dist/tooling/bundle-boundary.js +119 -0
  13. package/dist/web.js +605 -0
  14. package/package.json +179 -0
  15. package/skills/direct/AGENTS.md +13 -0
  16. package/skills/direct/SKILL.md +49 -0
  17. package/skills/direct/agents/openai.yaml +4 -0
  18. package/skills/direct/references/adoption.md +131 -0
  19. package/skills/direct/references/install.md +91 -0
  20. package/skills/direct/references/verification.md +247 -0
  21. package/src/core/coverage.ts +336 -0
  22. package/src/core/definition.ts +378 -0
  23. package/src/core/effects.ts +88 -0
  24. package/src/core/fixture.ts +185 -0
  25. package/src/core/ids.ts +77 -0
  26. package/src/core/index.ts +13 -0
  27. package/src/core/json-value.ts +7 -0
  28. package/src/core/json.ts +593 -0
  29. package/src/core/query.ts +230 -0
  30. package/src/core/reason.ts +16 -0
  31. package/src/core/resource.ts +10 -0
  32. package/src/core/result.ts +19 -0
  33. package/src/core/runtime.ts +229 -0
  34. package/src/core/scenario.ts +149 -0
  35. package/src/core/store.ts +784 -0
  36. package/src/index.ts +51 -0
  37. package/src/react.ts +54 -0
  38. package/src/testing/activity.ts +228 -0
  39. package/src/testing/coverage-binding.ts +99 -0
  40. package/src/testing/evidence.ts +59 -0
  41. package/src/testing/index.ts +22 -0
  42. package/src/testing/manifest.ts +559 -0
  43. package/src/testing/probe.ts +446 -0
  44. package/src/testing/scripted-transport.ts +775 -0
  45. package/src/testing/session.ts +525 -0
  46. package/src/tooling/bombadil-campaign.ts +288 -0
  47. package/src/tooling/bombadil-internal.d.ts +46 -0
  48. package/src/tooling/bombadil-runner.ts +1424 -0
  49. package/src/tooling/bombadil.ts +27 -0
  50. package/src/tooling/browser-verification-entry.ts +32 -0
  51. package/src/tooling/browser-verification.ts +916 -0
  52. package/src/tooling/bundle-boundary.ts +159 -0
  53. package/src/web/browser-bridge.ts +296 -0
  54. package/src/web/browser.ts +277 -0
  55. package/src/web/fetch-firewall.ts +251 -0
  56. package/src/web.ts +27 -0
@@ -0,0 +1,277 @@
1
+ import type { JsonValue } from "../core/json-value.js";
2
+ import { renderUnknownReason } from "../core/reason.js";
3
+ import { err, ok, type Result } from "../core/result.js";
4
+ import type { DirectActivityScopeError } from "../testing/activity.js";
5
+ import type { DirectProbe } from "../testing/probe.js";
6
+ import type {
7
+ DirectSession,
8
+ DirectSessionRegistrationError,
9
+ } from "../testing/session.js";
10
+ import {
11
+ prepareDirectBrowserBridgeInstallation,
12
+ type DirectBrowserBridgeError,
13
+ type DirectBrowserBridgeUninstall,
14
+ type PreparedDirectBrowserBridgeInstallation,
15
+ } from "./browser-bridge.js";
16
+ import {
17
+ prepareDirectFetchFirewallInstallation,
18
+ type DirectFetchFirewallOptions,
19
+ type DirectFetchFirewallUninstall,
20
+ type PreparedDirectFetchFirewallInstallation,
21
+ } from "./fetch-firewall.js";
22
+
23
+ export interface DirectBrowserFirewallOptions extends Omit<
24
+ DirectFetchFirewallOptions,
25
+ "beginActivity"
26
+ > {
27
+ /** Observe activity bookkeeping failures without allowing them to escape the fetch boundary. */
28
+ readonly onActivityError?: (error: DirectActivityScopeError) => void;
29
+ }
30
+
31
+ export interface InstallDirectBrowserOptions<
32
+ World extends JsonValue,
33
+ Route extends string,
34
+ Harness,
35
+ > {
36
+ /** The installation registers its teardown with this session. */
37
+ readonly session: DirectSession<World, Route, Harness>;
38
+ readonly reset?: () => undefined;
39
+ readonly target?: object;
40
+ /** Fail closed by default. Set false only when another boundary owns application fetch. */
41
+ readonly firewall?: DirectBrowserFirewallOptions | false;
42
+ }
43
+
44
+ export type DirectBrowserInstallError =
45
+ | {
46
+ readonly code: "invalid-options" | "firewall-install-failed";
47
+ readonly message: string;
48
+ readonly bridgeError: null;
49
+ readonly registrationError: null;
50
+ readonly rollbackErrors: readonly string[];
51
+ }
52
+ | {
53
+ readonly code: "bridge-install-failed";
54
+ readonly message: string;
55
+ readonly bridgeError: DirectBrowserBridgeError;
56
+ readonly registrationError: null;
57
+ readonly rollbackErrors: readonly string[];
58
+ }
59
+ | {
60
+ readonly code: "session-registration-failed";
61
+ readonly message: string;
62
+ readonly bridgeError: null;
63
+ readonly registrationError: DirectSessionRegistrationError;
64
+ readonly rollbackErrors: readonly string[];
65
+ }
66
+ | {
67
+ readonly code: "session-registration-threw";
68
+ readonly message: string;
69
+ readonly bridgeError: null;
70
+ readonly registrationError: null;
71
+ readonly rollbackErrors: readonly string[];
72
+ };
73
+
74
+ export interface DirectBrowserInstallation {
75
+ /** Uninstall browser globals and the fetch firewall without disposing the owning session. */
76
+ readonly dispose: () => undefined;
77
+ readonly isDisposed: () => boolean;
78
+ /** Best-effort disposal always attempts both browser boundaries. */
79
+ readonly disposalErrors: () => readonly string[];
80
+ }
81
+
82
+ function freezeMessages(messages: readonly string[]): readonly string[] {
83
+ return Object.freeze([...messages]);
84
+ }
85
+
86
+ function containPromiseLike(value: unknown): boolean {
87
+ if ((typeof value !== "object" || value === null) && typeof value !== "function") return false;
88
+ let then: unknown;
89
+ try {
90
+ then = Reflect.get(value, "then");
91
+ } catch {
92
+ return false;
93
+ }
94
+ if (typeof then !== "function") return false;
95
+ try {
96
+ void Promise.resolve(value).catch(() => undefined);
97
+ } catch {
98
+ // Promise assimilation is foreign code. Reporting remains observational.
99
+ }
100
+ return true;
101
+ }
102
+
103
+ function notifyActivityError(
104
+ observer: ((error: DirectActivityScopeError) => void) | undefined,
105
+ error: DirectActivityScopeError,
106
+ ): void {
107
+ if (observer === undefined) return;
108
+ try {
109
+ const returned: unknown = observer(error);
110
+ containPromiseLike(returned);
111
+ } catch {
112
+ // Reporting cannot weaken the fail-closed activity boundary.
113
+ }
114
+ }
115
+
116
+ function runBrowserCleanup(
117
+ bridge: DirectBrowserBridgeUninstall | null,
118
+ firewall: DirectFetchFirewallUninstall | null,
119
+ ): readonly string[] {
120
+ const errors: string[] = [];
121
+ for (const [label, cleanup] of [
122
+ ["bridge", bridge],
123
+ ["firewall", firewall],
124
+ ] as const) {
125
+ if (cleanup === null) continue;
126
+ try {
127
+ const returned: unknown = cleanup();
128
+ if (returned !== undefined) {
129
+ containPromiseLike(returned);
130
+ errors.push(`Direct browser ${label} cleanup must return undefined`);
131
+ }
132
+ } catch (reason) {
133
+ errors.push(renderUnknownReason(reason, `Direct browser ${label} cleanup failed`));
134
+ }
135
+ }
136
+ return freezeMessages(errors);
137
+ }
138
+
139
+ /**
140
+ * Install the exact session-manifest, probe, and reset browser bridge plus the
141
+ * fail-closed fetch boundary around one session. Installation is failure-atomic
142
+ * and teardown is registered with the session, so session disposal remains the
143
+ * aggregate lifecycle boundary.
144
+ */
145
+ export function installDirectBrowser<
146
+ World extends JsonValue,
147
+ Route extends string,
148
+ Harness,
149
+ >(
150
+ options: InstallDirectBrowserOptions<World, Route, Harness>,
151
+ ): Result<DirectBrowserInstallation, DirectBrowserInstallError> {
152
+ let activity: DirectSession<World, Route, Harness>["activity"];
153
+ let manifest: DirectSession<World, Route, Harness>["manifest"];
154
+ let onDispose: DirectSession<World, Route, Harness>["onDispose"];
155
+ let probe: DirectProbe;
156
+ let reset: (() => undefined) | undefined;
157
+ let target: object | undefined;
158
+ let firewallOptions: DirectBrowserFirewallOptions | false;
159
+ try {
160
+ const session = options.session;
161
+ activity = session.activity;
162
+ manifest = session.manifest;
163
+ onDispose = session.onDispose;
164
+ probe = session.probe;
165
+ reset = options.reset;
166
+ target = options.target;
167
+ firewallOptions = options.firewall ?? {};
168
+ } catch (reason) {
169
+ return err(Object.freeze({
170
+ code: "invalid-options",
171
+ message: renderUnknownReason(reason, "Direct browser options could not be inspected"),
172
+ bridgeError: null,
173
+ registrationError: null,
174
+ rollbackErrors: freezeMessages([]),
175
+ }));
176
+ }
177
+
178
+ let preparedFirewall: PreparedDirectFetchFirewallInstallation | null = null;
179
+ if (firewallOptions !== false) {
180
+ try {
181
+ const { onActivityError, ...lowLevelOptions } = firewallOptions;
182
+ preparedFirewall = prepareDirectFetchFirewallInstallation({
183
+ ...lowLevelOptions,
184
+ beginActivity: () => {
185
+ const started = activity.begin("browser-fetch");
186
+ if (!started.ok) {
187
+ notifyActivityError(onActivityError, started.error);
188
+ throw new Error(started.error.message, { cause: started.error });
189
+ }
190
+ return (): undefined => {
191
+ const released = started.value.release();
192
+ if (!released.ok) notifyActivityError(onActivityError, released.error);
193
+ return undefined;
194
+ };
195
+ },
196
+ });
197
+ } catch (reason) {
198
+ return err(Object.freeze({
199
+ code: "firewall-install-failed",
200
+ message: renderUnknownReason(reason, "Direct fetch firewall installation failed"),
201
+ bridgeError: null,
202
+ registrationError: null,
203
+ rollbackErrors: freezeMessages([]),
204
+ }));
205
+ }
206
+ }
207
+
208
+ const preparedBridgeResult = prepareDirectBrowserBridgeInstallation({
209
+ probe,
210
+ manifest,
211
+ ...(reset === undefined ? {} : { reset }),
212
+ ...(target === undefined ? {} : { target }),
213
+ });
214
+ if (!preparedBridgeResult.ok) {
215
+ const rollbackErrors = runBrowserCleanup(null, preparedFirewall?.rollback ?? null);
216
+ return err(Object.freeze({
217
+ code: "bridge-install-failed",
218
+ message: preparedBridgeResult.error.message,
219
+ bridgeError: preparedBridgeResult.error,
220
+ registrationError: null,
221
+ rollbackErrors,
222
+ }));
223
+ }
224
+ const preparedBridge: PreparedDirectBrowserBridgeInstallation = preparedBridgeResult.value;
225
+
226
+ let disposed = false;
227
+ let committed = false;
228
+ let disposalErrors: readonly string[] = freezeMessages([]);
229
+ const dispose = (): undefined => {
230
+ if (disposed) return undefined;
231
+ disposed = true;
232
+ disposalErrors = committed
233
+ ? runBrowserCleanup(preparedBridge.uninstall, preparedFirewall?.uninstall ?? null)
234
+ : runBrowserCleanup(preparedBridge.rollback, preparedFirewall?.rollback ?? null);
235
+ return undefined;
236
+ };
237
+ const installation: DirectBrowserInstallation = Object.freeze({
238
+ dispose,
239
+ isDisposed: () => disposed,
240
+ disposalErrors: () => disposalErrors,
241
+ });
242
+ try {
243
+ const registered = onDispose(dispose);
244
+ if (!registered.ok) {
245
+ dispose();
246
+ return err(Object.freeze({
247
+ code: "session-registration-failed",
248
+ message: registered.error.message,
249
+ bridgeError: null,
250
+ registrationError: registered.error,
251
+ rollbackErrors: disposalErrors,
252
+ }));
253
+ }
254
+ } catch (reason) {
255
+ dispose();
256
+ return err(Object.freeze({
257
+ code: "session-registration-threw",
258
+ message: renderUnknownReason(reason, "Direct session cleanup registration failed"),
259
+ bridgeError: null,
260
+ registrationError: null,
261
+ rollbackErrors: disposalErrors,
262
+ }));
263
+ }
264
+ if (disposed) {
265
+ return err(Object.freeze({
266
+ code: "session-registration-threw",
267
+ message: "Direct session disposed browser ownership during cleanup registration",
268
+ bridgeError: null,
269
+ registrationError: null,
270
+ rollbackErrors: disposalErrors,
271
+ }));
272
+ }
273
+ preparedFirewall?.commit();
274
+ preparedBridge.commit();
275
+ committed = true;
276
+ return ok(installation);
277
+ }
@@ -0,0 +1,251 @@
1
+ export interface DirectFetchFirewallOptions {
2
+ readonly allow?: (url: URL) => boolean;
3
+ readonly beginActivity?: (url: URL | null) => () => void;
4
+ readonly onBlocked?: (url: URL | null) => void;
5
+ readonly originalFetch?: FetchCall;
6
+ }
7
+
8
+ type FetchCall = (...arguments_: Parameters<typeof fetch>) => ReturnType<typeof fetch>;
9
+
10
+ export type DirectFetchFirewallUninstall = () => undefined;
11
+
12
+ export interface PreparedDirectFetchFirewallInstallation {
13
+ readonly commit: () => undefined;
14
+ readonly rollback: () => undefined;
15
+ readonly uninstall: DirectFetchFirewallUninstall;
16
+ }
17
+
18
+ interface ActiveFetchFirewallInstallation {
19
+ readonly guardedFetch: typeof fetch;
20
+ readonly restoreFetch: typeof fetch;
21
+ readonly deactivate: DirectFetchFirewallUninstall;
22
+ readonly uninstall: DirectFetchFirewallUninstall;
23
+ }
24
+
25
+ interface ActivityBoundary {
26
+ readonly valid: boolean;
27
+ readonly release: () => undefined;
28
+ }
29
+
30
+ let activeFirewallInstallation: ActiveFetchFirewallInstallation | null = null;
31
+
32
+ function containPromiseLike(value: unknown): boolean {
33
+ if ((typeof value !== "object" || value === null) && typeof value !== "function") return false;
34
+ let then: unknown;
35
+ try {
36
+ then = Reflect.get(value, "then");
37
+ } catch {
38
+ return false;
39
+ }
40
+ if (typeof then !== "function") return false;
41
+ try {
42
+ void Promise.resolve(value).catch(() => undefined);
43
+ } catch {
44
+ // Promise assimilation is foreign code. Treat it as contained and invalid.
45
+ }
46
+ return true;
47
+ }
48
+
49
+ function requestUrl(input: unknown): URL | null {
50
+ try {
51
+ if (typeof Request !== "undefined" && input instanceof Request) return new URL(input.url);
52
+ const browserGlobal = globalThis as typeof globalThis & { readonly location?: { readonly origin?: string } };
53
+ const locationOrigin = browserGlobal.location?.origin;
54
+ const base = locationOrigin === undefined || locationOrigin === "null"
55
+ ? "http://direct.invalid"
56
+ : locationOrigin;
57
+ return new URL(String(input), base);
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ function blockedResponse(): Response {
64
+ return new Response(
65
+ JSON.stringify({ error: "Direct blocked an unmapped network request." }),
66
+ { status: 501, headers: { "content-type": "application/json" } },
67
+ );
68
+ }
69
+
70
+ function beginBoundary(
71
+ beginActivity: ((url: URL | null) => () => void) | undefined,
72
+ url: URL | null,
73
+ ): ActivityBoundary {
74
+ if (beginActivity === undefined) {
75
+ return { valid: true, release: () => undefined };
76
+ }
77
+ let candidate: unknown;
78
+ try {
79
+ candidate = beginActivity(url);
80
+ } catch {
81
+ return { valid: false, release: () => undefined };
82
+ }
83
+ if (containPromiseLike(candidate) || typeof candidate !== "function") {
84
+ return { valid: false, release: () => undefined };
85
+ }
86
+ let active = true;
87
+ return {
88
+ valid: true,
89
+ release: (): undefined => {
90
+ if (!active) return undefined;
91
+ active = false;
92
+ try {
93
+ const returned: unknown = Reflect.apply(candidate, undefined, []);
94
+ containPromiseLike(returned);
95
+ } catch {
96
+ // Activity cleanup cannot turn a blocked request into an unhandled failure.
97
+ }
98
+ return undefined;
99
+ },
100
+ };
101
+ }
102
+
103
+ function explicitlyAllowed(
104
+ allow: ((url: URL) => boolean) | undefined,
105
+ url: URL | null,
106
+ ): boolean {
107
+ if (allow === undefined || url === null) return false;
108
+ try {
109
+ const returned: unknown = allow(url);
110
+ if (containPromiseLike(returned)) return false;
111
+ return returned === true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ function notifyBlocked(
118
+ onBlocked: ((url: URL | null) => void) | undefined,
119
+ url: URL | null,
120
+ ): void {
121
+ if (onBlocked === undefined) return;
122
+ try {
123
+ const returned: unknown = onBlocked(url);
124
+ containPromiseLike(returned);
125
+ } catch {
126
+ // Reporting is observational and cannot bypass or reject the firewall.
127
+ }
128
+ }
129
+
130
+ /** Prepare a reversible replacement without deactivating the current owner. */
131
+ export function prepareDirectFetchFirewallInstallation(
132
+ options: DirectFetchFirewallOptions = {},
133
+ ): PreparedDirectFetchFirewallInstallation {
134
+ const previousInstallation = activeFirewallInstallation;
135
+ const currentFetch = globalThis.fetch;
136
+ const previousOwnsCurrent = previousInstallation?.guardedFetch === currentFetch;
137
+ const restoreFetch = previousOwnsCurrent
138
+ ? previousInstallation.restoreFetch
139
+ : currentFetch;
140
+
141
+ // Capture every foreign option before mutating the global. A throwing getter
142
+ // therefore leaves an already-installed firewall intact.
143
+ const allow = options.allow;
144
+ const beginActivity = options.beginActivity;
145
+ const onBlocked = options.onBlocked;
146
+ const originalFetch: FetchCall = options.originalFetch ?? restoreFetch;
147
+ if (typeof originalFetch !== "function") {
148
+ throw new TypeError("Direct fetch firewall requires a callable fetch implementation");
149
+ }
150
+
151
+ const guardedCall: FetchCall = async (input, init) => {
152
+ const url = requestUrl(input);
153
+ const activity = beginBoundary(beginActivity, url);
154
+ try {
155
+ if (activity.valid && explicitlyAllowed(allow, url)) {
156
+ return await originalFetch(input, init);
157
+ }
158
+ notifyBlocked(onBlocked, url);
159
+ return blockedResponse();
160
+ } finally {
161
+ activity.release();
162
+ }
163
+ };
164
+
165
+ const previousPreconnect: unknown = Reflect.get(restoreFetch, "preconnect");
166
+ if (typeof previousPreconnect === "function") {
167
+ Object.defineProperty(guardedCall, "preconnect", {
168
+ configurable: true,
169
+ value: (...arguments_: unknown[]): undefined => {
170
+ const url = requestUrl(arguments_[0]);
171
+ const activity = beginBoundary(beginActivity, url);
172
+ try {
173
+ if (activity.valid && explicitlyAllowed(allow, url)) {
174
+ const returned: unknown = Reflect.apply(previousPreconnect, restoreFetch, arguments_);
175
+ containPromiseLike(returned);
176
+ } else {
177
+ notifyBlocked(onBlocked, url);
178
+ }
179
+ } finally {
180
+ activity.release();
181
+ }
182
+ return undefined;
183
+ },
184
+ });
185
+ }
186
+
187
+ const guardedFetch = guardedCall as typeof fetch;
188
+ try {
189
+ globalThis.fetch = guardedFetch;
190
+ if (globalThis.fetch !== guardedFetch) {
191
+ throw new TypeError("Direct fetch firewall could not replace global fetch");
192
+ }
193
+ } catch (reason) {
194
+ try {
195
+ if (globalThis.fetch === guardedFetch) globalThis.fetch = currentFetch;
196
+ } catch {
197
+ // Best-effort rollback on a hostile global; the original error is more useful.
198
+ }
199
+ throw reason;
200
+ }
201
+
202
+ let state: "prepared" | "committed" | "closed" = "prepared";
203
+ const rollback = (): undefined => {
204
+ if (state !== "prepared") return undefined;
205
+ if (globalThis.fetch === guardedFetch) globalThis.fetch = currentFetch;
206
+ state = "closed";
207
+ return undefined;
208
+ };
209
+ const deactivate = (): undefined => {
210
+ if (state !== "committed") return undefined;
211
+ state = "closed";
212
+ if (activeFirewallInstallation === installation) activeFirewallInstallation = null;
213
+ return undefined;
214
+ };
215
+ const uninstall = (): undefined => {
216
+ if (state === "prepared") return rollback();
217
+ if (state !== "committed") return undefined;
218
+ if (globalThis.fetch === guardedFetch) globalThis.fetch = restoreFetch;
219
+ state = "closed";
220
+ if (activeFirewallInstallation === installation) activeFirewallInstallation = null;
221
+ return undefined;
222
+ };
223
+ const installation: ActiveFetchFirewallInstallation = {
224
+ guardedFetch,
225
+ restoreFetch,
226
+ deactivate,
227
+ uninstall,
228
+ };
229
+ const commit = (): undefined => {
230
+ if (state !== "prepared") return undefined;
231
+ state = "committed";
232
+ if (previousInstallation !== null) previousInstallation.deactivate();
233
+ activeFirewallInstallation = installation;
234
+ return undefined;
235
+ };
236
+
237
+ return Object.freeze({
238
+ commit,
239
+ rollback,
240
+ uninstall,
241
+ });
242
+ }
243
+
244
+ /** Install one process-local, fail-closed fetch boundary for a browser Direct frame. */
245
+ export function installDirectFetchFirewall(
246
+ options: DirectFetchFirewallOptions = {},
247
+ ): DirectFetchFirewallUninstall {
248
+ const prepared = prepareDirectFetchFirewallInstallation(options);
249
+ prepared.commit();
250
+ return prepared.uninstall;
251
+ }
package/src/web.ts ADDED
@@ -0,0 +1,27 @@
1
+ import {
2
+ DIRECT_BROWSER_BRIDGE_SCHEMA as browserBridgeSchema,
3
+ installDirectBrowserBridge as installBrowserBridge,
4
+ } from "./web/browser-bridge.js";
5
+ import {
6
+ installDirectFetchFirewall as installFetchFirewall,
7
+ } from "./web/fetch-firewall.js";
8
+
9
+ export * from "./web/browser.js";
10
+ export const DIRECT_BROWSER_BRIDGE_SCHEMA = browserBridgeSchema;
11
+ export const installDirectBrowserBridge: typeof installBrowserBridge = (
12
+ options,
13
+ ) => installBrowserBridge(options);
14
+ export type {
15
+ DirectBrowserBridge,
16
+ DirectBrowserBridgeError,
17
+ DirectBrowserBridgeErrorCode,
18
+ DirectBrowserBridgeOptions,
19
+ DirectBrowserBridgeUninstall,
20
+ } from "./web/browser-bridge.js";
21
+ export const installDirectFetchFirewall: typeof installFetchFirewall = (
22
+ options,
23
+ ) => installFetchFirewall(options);
24
+ export type {
25
+ DirectFetchFirewallOptions,
26
+ DirectFetchFirewallUninstall,
27
+ } from "./web/fetch-firewall.js";