@expandai/sdk 0.20.1 → 0.20.2

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 (51) hide show
  1. package/README.md +11 -0
  2. package/build/dts/Error.d.ts +25 -1
  3. package/build/dts/Error.d.ts.map +1 -1
  4. package/build/dts/Internal.d.ts.map +1 -1
  5. package/build/dts/RetryAfter.d.ts +2 -0
  6. package/build/dts/RetryAfter.d.ts.map +1 -1
  7. package/build/dts/Service.d.ts +9 -2
  8. package/build/dts/Service.d.ts.map +1 -1
  9. package/build/dts/Tunnel/Headers.d.ts +2 -0
  10. package/build/dts/Tunnel/Headers.d.ts.map +1 -0
  11. package/build/dts/Tunnel/LocalTarget.d.ts +122 -0
  12. package/build/dts/Tunnel/LocalTarget.d.ts.map +1 -0
  13. package/build/dts/Tunnel/Protocol.d.ts +160 -0
  14. package/build/dts/Tunnel/Protocol.d.ts.map +1 -0
  15. package/build/dts/Tunnel/StaticServer.d.ts +10 -0
  16. package/build/dts/Tunnel/StaticServer.d.ts.map +1 -0
  17. package/build/dts/Tunnel/Tunnel.d.ts +40 -0
  18. package/build/dts/Tunnel/Tunnel.d.ts.map +1 -0
  19. package/build/dts/index.d.ts +2 -2
  20. package/build/dts/index.d.ts.map +1 -1
  21. package/build/esm/Error.js +27 -0
  22. package/build/esm/Error.js.map +1 -1
  23. package/build/esm/Internal.js +9 -1
  24. package/build/esm/Internal.js.map +1 -1
  25. package/build/esm/RetryAfter.js +3 -1
  26. package/build/esm/RetryAfter.js.map +1 -1
  27. package/build/esm/Service.js +23 -7
  28. package/build/esm/Service.js.map +1 -1
  29. package/build/esm/Tunnel/Headers.js +18 -0
  30. package/build/esm/Tunnel/Headers.js.map +1 -0
  31. package/build/esm/Tunnel/LocalTarget.js +125 -0
  32. package/build/esm/Tunnel/LocalTarget.js.map +1 -0
  33. package/build/esm/Tunnel/Protocol.js +73 -0
  34. package/build/esm/Tunnel/Protocol.js.map +1 -0
  35. package/build/esm/Tunnel/StaticServer.js +69 -0
  36. package/build/esm/Tunnel/StaticServer.js.map +1 -0
  37. package/build/esm/Tunnel/Tunnel.js +302 -0
  38. package/build/esm/Tunnel/Tunnel.js.map +1 -0
  39. package/build/esm/index.js +1 -1
  40. package/build/esm/index.js.map +1 -1
  41. package/package.json +6 -3
  42. package/src/Error.ts +53 -1
  43. package/src/Internal.ts +10 -0
  44. package/src/RetryAfter.ts +4 -1
  45. package/src/Service.ts +48 -8
  46. package/src/Tunnel/Headers.ts +22 -0
  47. package/src/Tunnel/LocalTarget.ts +153 -0
  48. package/src/Tunnel/Protocol.ts +99 -0
  49. package/src/Tunnel/StaticServer.ts +85 -0
  50. package/src/Tunnel/Tunnel.ts +420 -0
  51. package/src/index.ts +3 -1
@@ -0,0 +1,302 @@
1
+ import { createServer } from 'node:http';
2
+ import { Cookies, FetchHttpClient, HttpBody, HttpClient, HttpClientRequest, Path, Socket, } from '@effect/platform';
3
+ import { NodeFileSystem, NodeHttpServer, NodePath } from '@effect/platform-node';
4
+ import { Config, Deferred, Duration, Effect, FiberMap, Iterable, Layer, Match, Option, Schema, Stream } from 'effect';
5
+ import { WebSocket } from 'ws';
6
+ import { ExpandLocalTargetError } from '../Error.js';
7
+ import { endToEndHeaders } from './Headers.js';
8
+ import { LocalTarget, resolveLocalTarget, resolveSafeDirectory } from './LocalTarget.js';
9
+ import { decodeBodyFrame, encodeBodyFrame, encodeClientToRelay, MAX_BODY_BYTES, MAX_INFLIGHT_REQUESTS, PROTOCOL_VERSION, RelayToClient, splitBodyChunk, } from './Protocol.js';
10
+ import { serveDirectory, serveFile } from './StaticServer.js';
11
+ export class TunnelError extends Schema.TaggedError()('TunnelError', {
12
+ reason: Schema.Literal('Unauthorized', 'Configuration', 'RelayUnreachable'),
13
+ message: Schema.String,
14
+ cause: Schema.optional(Schema.Defect),
15
+ }) {
16
+ }
17
+ const HELLO_TIMEOUT = Duration.seconds(10);
18
+ const MAX_QUEUED_RELAY_BYTES = 16 * 1024 * 1024;
19
+ // A masked WebSocket frame has at most 14 bytes of framing in addition to its payload.
20
+ const MAX_WEBSOCKET_FRAME_OVERHEAD = 14;
21
+ const RELAY_URLS = new Map([
22
+ ['https://api.expand.ai', 'https://expandusercontent.com'],
23
+ ['https://api.staging.expand.ai', 'https://staging.expandusercontent.com'],
24
+ ]);
25
+ const decodeControlFrame = Schema.decodeUnknownOption(Schema.parseJson(RelayToClient));
26
+ const decodeBody = Option.liftThrowable(decodeBodyFrame);
27
+ // Hop-by-hop or recomputed by `fetch`; forwarding them breaks the local request.
28
+ const DROPPED_REQUEST_HEADERS = new Set(['host', 'content-length', 'accept-encoding']);
29
+ // `fetch` decoded the body; cookies are forwarded separately to preserve repeated fields.
30
+ const DROPPED_RESPONSE_HEADERS = new Set(['content-encoding', 'content-length', 'set-cookie']);
31
+ function rewriteLocalLocation(value, localUrl, publicUrl) {
32
+ if (!URL.canParse(value))
33
+ return value;
34
+ const location = new URL(value);
35
+ return location.origin === new URL(localUrl).origin
36
+ ? `${publicUrl}${location.pathname}${location.search}${location.hash}`
37
+ : value;
38
+ }
39
+ // `fetch` reports "fetch failed" and hides the reason in `cause`; a refused connection is an
40
+ // `AggregateError` (one attempt per address) with an empty message and the reason in `code`.
41
+ function describeFetchError(error) {
42
+ const cause = error instanceof Error && error.cause instanceof Error ? error.cause : error;
43
+ if (!(cause instanceof Error))
44
+ return String(cause);
45
+ return cause.message || ('code' in cause && typeof cause.code === 'string' ? cause.code : cause.name);
46
+ }
47
+ /** Uses the relay for the selected API environment, unless `EXPAND_TUNNEL_URL` says otherwise. */
48
+ const relayUrlFor = Effect.fn('Tunnel.relayUrlFor')(function* (baseUrl) {
49
+ const override = yield* Config.option(Config.string('EXPAND_TUNNEL_URL')).pipe(Effect.orDie);
50
+ const configured = Option.orElse(override, () => Option.fromNullable(RELAY_URLS.get(baseUrl.replace(/\/+$/u, ''))));
51
+ if (Option.isNone(configured)) {
52
+ return yield* new TunnelError({
53
+ reason: 'Configuration',
54
+ message: `No tunnel relay is known for ${baseUrl}; set EXPAND_TUNNEL_URL.`,
55
+ });
56
+ }
57
+ const relayUrl = configured.value;
58
+ if (!URL.canParse(relayUrl) || !['http:', 'https:'].includes(new URL(relayUrl).protocol)) {
59
+ return yield* new TunnelError({
60
+ reason: 'Configuration',
61
+ message: 'EXPAND_TUNNEL_URL must be an http:// or https:// relay address.',
62
+ });
63
+ }
64
+ return relayUrl.replace(/\/+$/u, '');
65
+ });
66
+ function tunnelOpenError(relayUrl, status) {
67
+ if (status === 401 || status === 403) {
68
+ return new TunnelError({ reason: 'Unauthorized', message: 'The tunnel relay rejected the Expand credentials.' });
69
+ }
70
+ const retryable = status === 0 || status === 408 || status === 429 || status >= 500;
71
+ return new TunnelError({
72
+ reason: retryable ? 'RelayUnreachable' : 'Configuration',
73
+ message: `Could not open a tunnel at ${relayUrl}${status === 0 ? '' : ` (HTTP ${status})`}.`,
74
+ });
75
+ }
76
+ // Effect owns receiving and socket lifetime; native send callbacks supply the backpressure its writer lacks.
77
+ const makeRelaySocket = Effect.fn('Tunnel.makeRelaySocket')(function* (relayUrl, headers) {
78
+ const opened = yield* Deferred.make();
79
+ let rejectedStatus = 0;
80
+ const acquire = Effect.acquireRelease(Effect.sync(() => {
81
+ // Disable compression so bufferedAmount plus frame overhead bounds the actual native send queue.
82
+ const ws = new WebSocket(`${relayUrl.replace(/^http/u, 'ws')}/v1/connect`, { headers, perMessageDeflate: false });
83
+ ws.once('open', () => Deferred.unsafeDone(opened, Effect.succeed(ws)));
84
+ ws.once('unexpected-response', (_request, response) => {
85
+ rejectedStatus = response.statusCode ?? 0;
86
+ ws.terminate();
87
+ });
88
+ return ws;
89
+ }),
90
+ // Graceful close queues behind body frames and can retain them for ws's 30-second close timeout.
91
+ (ws) => Effect.sync(() => ws.terminate()));
92
+ const socket = yield* Socket.fromWebSocket(
93
+ // `ws` implements the WHATWG event interface `fromWebSocket` uses.
94
+ acquire.pipe(Effect.map((ws) => ws)));
95
+ const writer = Effect.map(socket.writer, (closeWrite) => Effect.fnUntraced(function* (chunk) {
96
+ if (Socket.isCloseEvent(chunk))
97
+ return yield* closeWrite(chunk);
98
+ const ws = yield* Deferred.await(opened);
99
+ return yield* Effect.async((resume) => {
100
+ const fail = (cause) => Effect.fail(new Socket.SocketGenericError({ reason: 'Write', cause }));
101
+ if (ws.readyState !== WebSocket.OPEN) {
102
+ resume(fail(new Error('Tunnel connection closed.')));
103
+ return;
104
+ }
105
+ const bytes = typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.byteLength;
106
+ if (ws.bufferedAmount + bytes + MAX_WEBSOCKET_FRAME_OVERHEAD > MAX_QUEUED_RELAY_BYTES) {
107
+ // Cancelled requests leave native writes queued, so overflow must end the whole transport.
108
+ ws.terminate();
109
+ resume(fail(new Error('Tunnel relay stopped reading.')));
110
+ return;
111
+ }
112
+ let complete = resume;
113
+ try {
114
+ ws.send(chunk, (error) => {
115
+ complete?.(error ? fail(error) : Effect.void);
116
+ complete = undefined;
117
+ });
118
+ }
119
+ catch (error) {
120
+ complete = undefined;
121
+ resume(fail(error));
122
+ }
123
+ // ws retains the send callback until flushing or close; don't retain an interrupted request fiber.
124
+ return Effect.sync(() => {
125
+ complete = undefined;
126
+ });
127
+ });
128
+ }));
129
+ return { socket: Socket.Socket.of({ ...socket, writer }), rejectedStatus: () => rejectedStatus };
130
+ });
131
+ // Where the relay's requests go, and the path on the public URL that shows the target.
132
+ const localOrigin = Effect.fn('Tunnel.localOrigin')(function* (target) {
133
+ if (LocalTarget.$is('Port')(target))
134
+ return { host: target.host, port: target.port, path: target.path };
135
+ const path = yield* Path.Path;
136
+ const isFile = LocalTarget.$is('File')(target);
137
+ const directory = isFile ? target.path : yield* resolveSafeDirectory(target.path);
138
+ // Build into the tunnel's scope so the listener stays open after this function returns.
139
+ const port = yield* Layer.build(NodeHttpServer.layer(() => createServer(), { host: '127.0.0.1', port: 0 })).pipe(Effect.flatMap((context) => (isFile ? serveFile(target.path) : serveDirectory(directory)).pipe(Effect.provide(context))), Effect.mapError((cause) => new ExpandLocalTargetError({
140
+ reason: 'Unavailable',
141
+ input: target.path,
142
+ cause,
143
+ message: `Could not start a local file server for "${target.path}". ${cause.message}`,
144
+ })));
145
+ return { host: '127.0.0.1', port, path: isFile ? `/${encodeURIComponent(path.basename(target.path))}` : '/' };
146
+ }, Effect.provide(Layer.merge(NodePath.layer, NodeFileSystem.layer)));
147
+ const makeResponder = Effect.fn('Tunnel.makeResponder')(function* (socket, localUrl, hello) {
148
+ // Loopback requests must not inherit API proxies or trace user paths and forwarded credentials.
149
+ const http = HttpClient.withScope(yield* HttpClient.HttpClient).pipe(HttpClient.withTracerDisabledWhen(() => true), HttpClient.withTracerPropagation(false));
150
+ const write = yield* socket.writer;
151
+ const send = (frame) => write(encodeClientToRelay(frame));
152
+ return Effect.fn('Tunnel.respond')(function* (start, body) {
153
+ const { url: publicUrl } = yield* Deferred.await(hello);
154
+ const response = yield* http
155
+ .execute(
156
+ // Effect's method union is narrower than fetch; preserve custom methods from the relay.
157
+ HttpClientRequest.make(start.method)(`${localUrl}${start.path}`, {
158
+ headers: endToEndHeaders(start.headers).filter(([name]) => !DROPPED_REQUEST_HEADERS.has(name.toLowerCase())),
159
+ body: Option.isSome(body) ? HttpBody.raw(body.value) : undefined,
160
+ }))
161
+ .pipe(Effect.mapError((error) => describeFetchError(error.cause ?? error)));
162
+ yield* send({
163
+ _tag: 'ResponseStart',
164
+ v: PROTOCOL_VERSION,
165
+ id: start.id,
166
+ status: response.status,
167
+ headers: endToEndHeaders(Object.entries(response.headers))
168
+ .filter(([name]) => !DROPPED_RESPONSE_HEADERS.has(name))
169
+ // A dev server redirecting to its own absolute URL must send the fetcher back through the tunnel.
170
+ .map(([name, value]) => [
171
+ name,
172
+ name === 'location' ? rewriteLocalLocation(value, localUrl, publicUrl) : value,
173
+ ])
174
+ // Effect's flat Headers record loses repeated Set-Cookie fields.
175
+ .concat(Cookies.toSetCookieHeaders(response.cookies).map((cookie) => ['set-cookie', cookie])),
176
+ });
177
+ let responseBytes = 0;
178
+ yield* response.stream.pipe(Stream.catchTag('ResponseError', (error) => error.reason === 'EmptyBody' ? Stream.empty : Stream.fail(describeFetchError(error.cause ?? error))), Stream.runForEach(Effect.fnUntraced(function* (chunk) {
179
+ responseBytes += chunk.byteLength;
180
+ // Enforce the response bound independently of transport backpressure and relay cancellation.
181
+ if (responseBytes > MAX_BODY_BYTES)
182
+ return yield* Effect.fail('response body too large');
183
+ yield* Effect.forEach(splitBodyChunk(chunk), (slice) => write(encodeBodyFrame(start.id, slice)));
184
+ })));
185
+ yield* send({ _tag: 'ResponseEnd', v: PROTOCOL_VERSION, id: start.id });
186
+ }, (effect, start) => effect.pipe(Effect.scoped, Effect.catchAll((error) => send({
187
+ _tag: 'ResponseError',
188
+ v: PROTOCOL_VERSION,
189
+ id: start.id,
190
+ message: `${localUrl} did not answer: ${String(error)}`,
191
+ })), Effect.ignore));
192
+ });
193
+ const runRequests = Effect.fn('Tunnel.runRequests')(function* (socket, localUrl, hello) {
194
+ const write = yield* socket.writer;
195
+ const send = (frame) => write(encodeClientToRelay(frame));
196
+ const requestFibers = yield* FiberMap.make();
197
+ const run = yield* FiberMap.runtime(requestFibers)();
198
+ const respond = yield* makeResponder(socket, localUrl, hello);
199
+ function runRequest(start, body) {
200
+ run(start.id, respond(start, body));
201
+ }
202
+ // Bookkeeping runs synchronously in frame order so body chunks always land after their
203
+ // `RequestStart` and before their `RequestEnd`; only the returned effects run concurrently.
204
+ const bodies = new Map();
205
+ function handleBodyFrame(data) {
206
+ const body = decodeBody(data);
207
+ if (Option.isNone(body))
208
+ return;
209
+ const pending = bodies.get(body.value.id);
210
+ if (pending === undefined)
211
+ return;
212
+ pending.chunks.push(body.value.chunk);
213
+ pending.bytes += body.value.chunk.byteLength;
214
+ // The public URL is open to anyone who learns it; do not let a request body exhaust memory.
215
+ if (pending.bytes <= MAX_BODY_BYTES)
216
+ return;
217
+ bodies.delete(body.value.id);
218
+ return send({
219
+ _tag: 'ResponseError',
220
+ v: PROTOCOL_VERSION,
221
+ id: body.value.id,
222
+ message: 'request body too large',
223
+ });
224
+ }
225
+ function handleControlFrame(data) {
226
+ return Option.match(decodeControlFrame(data), {
227
+ onNone: () => Effect.logDebug('Ignoring unrecognised tunnel frame'),
228
+ onSome: Match.valueTags({
229
+ Hello: (frame) => Deferred.succeed(hello, frame),
230
+ RequestStart: (start) => {
231
+ if (bodies.has(start.id) || FiberMap.unsafeHas(requestFibers, start.id))
232
+ return;
233
+ if (bodies.size + Iterable.size(requestFibers) >= MAX_INFLIGHT_REQUESTS) {
234
+ return send({
235
+ _tag: 'ResponseError',
236
+ v: PROTOCOL_VERSION,
237
+ id: start.id,
238
+ message: 'too many local requests',
239
+ });
240
+ }
241
+ if (!start.hasBody) {
242
+ runRequest(start, Option.none());
243
+ return;
244
+ }
245
+ bodies.set(start.id, { start, chunks: [], bytes: 0 });
246
+ return Effect.void;
247
+ },
248
+ RequestCancel: ({ id }) => {
249
+ bodies.delete(id);
250
+ return FiberMap.remove(requestFibers, id);
251
+ },
252
+ RequestEnd: ({ id }) => {
253
+ const pending = Option.fromNullable(bodies.get(id));
254
+ bodies.delete(id);
255
+ return Option.match(pending, {
256
+ onNone: () => Effect.void,
257
+ onSome: ({ start, chunks }) => {
258
+ runRequest(start, Option.some(Buffer.concat(chunks)));
259
+ return Effect.void;
260
+ },
261
+ });
262
+ },
263
+ }),
264
+ });
265
+ }
266
+ return yield* socket
267
+ .runRaw((data) => (typeof data === 'string' ? handleControlFrame(data) : handleBodyFrame(data)))
268
+ .pipe(Effect.ensuring(Effect.sync(() => bodies.clear())));
269
+ });
270
+ /**
271
+ * Opens a tunnel from the relay to `target` for the lifetime of the scope and returns its public URL.
272
+ * Closing the scope closes the socket and, for file targets, the static server.
273
+ */
274
+ export const openTunnel = Effect.fn('Tunnel.openTunnel')(function* (target, relayUrl, headers) {
275
+ const origin = yield* localOrigin(target);
276
+ const { socket, rejectedStatus } = yield* makeRelaySocket(relayUrl, headers);
277
+ const hello = yield* Deferred.make();
278
+ // If the socket ends before Hello the open fails; after Hello, the API simply sees the origin go away.
279
+ yield* runRequests(socket, `http://${origin.host}:${origin.port}`, hello).pipe(Effect.scoped, Effect.exit, Effect.flatMap(() => Deferred.fail(hello, tunnelOpenError(relayUrl, rejectedStatus()))), Effect.forkScoped);
280
+ const { url } = yield* Deferred.await(hello).pipe(Effect.timeoutFail({
281
+ duration: HELLO_TIMEOUT,
282
+ onTimeout: () => new TunnelError({
283
+ reason: 'RelayUnreachable',
284
+ message: `No answer from the tunnel relay at ${relayUrl}.`,
285
+ }),
286
+ }));
287
+ return `${url}${origin.path}`;
288
+ }, Effect.provide(FetchHttpClient.layer), Effect.provideService(FetchHttpClient.RequestInit, { redirect: 'manual' }));
289
+ /**
290
+ * Runs `use` with a public URL for `input`, which must be a local target (see `resolveLocalTarget`),
291
+ * and closes the tunnel afterwards.
292
+ */
293
+ export const withLocalTunnel = Effect.fn('Tunnel.withLocalTunnel')(function* (input, credentials, use) {
294
+ const target = yield* resolveLocalTarget(input).pipe(Effect.provide(Layer.merge(NodeFileSystem.layer, NodePath.layer)));
295
+ const { baseUrl, headers } = yield* credentials;
296
+ const relayUrl = yield* relayUrlFor(baseUrl);
297
+ const url = yield* openTunnel(target, relayUrl, headers);
298
+ return yield* use(url);
299
+ }, (effect, input) => effect.pipe(Effect.mapError((error) => error instanceof ExpandLocalTargetError && error.input !== input
300
+ ? new ExpandLocalTargetError({ reason: error.reason, input, message: error.message, cause: error.cause })
301
+ : error), Effect.scoped));
302
+ //# sourceMappingURL=Tunnel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tunnel.js","sourceRoot":"","sources":["../../../src/Tunnel/Tunnel.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  export { APPENDIX_MARKER_PREFIX, AppendixMarkerLevel, isAppendixMarkerLine, parseAppendixMarkerLine, parseAppendixMarkers, stripAppendixMarkers, withoutFencedCode, } from './AppendixMarkers.js';
2
2
  export { Expand, ExpandClient } from './Client.js';
3
3
  export * from './Crawls.js';
4
- export { ExpandApiError, ExpandClientApiError, ExpandClientConnectionError, ExpandClientError, ExpandClientTimeoutError, ExpandConnectionError, ExpandSdkError, ExpandTimeoutError, } from './Error.js';
4
+ export { ExpandApiError, ExpandClientApiError, ExpandClientConnectionError, ExpandClientError, ExpandClientLocalTargetError, ExpandClientTimeoutError, ExpandConnectionError, ExpandLocalTargetError, ExpandSdkError, ExpandTimeoutError, } from './Error.js';
5
5
  export { isFetchOriginError } from './FetchJson.js';
6
6
  export { citationUrl, DEFAULT_PLAYGROUND_HOST, parseSnapshotHandle, playgroundAppendixUrl, playgroundBase, playgroundContentUrl, resolvePlaygroundHost, } from './Playground.js';
7
7
  export { ExpandService, ExpandServiceOptions, RequestOptions } from './Service.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACN,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,GACjB,MAAM,sBAAsB,CAAA;AAqB7B,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAClD,cAAc,aAAa,CAAA;AAE3B,OAAO,EACN,cAAc,EACd,oBAAoB,EACpB,2BAA2B,EAC3B,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,EACd,kBAAkB,GAClB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,EACN,WAAW,EACX,uBAAuB,EACvB,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,oBAAoB,EACpB,qBAAqB,GACrB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACN,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,GACjB,MAAM,sBAAsB,CAAA;AAqB7B,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAClD,cAAc,aAAa,CAAA;AAE3B,OAAO,EACN,cAAc,EACd,oBAAoB,EACpB,2BAA2B,EAC3B,iBAAiB,EACjB,4BAA4B,EAC5B,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EACtB,cAAc,EACd,kBAAkB,GAClB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,EACN,WAAW,EACX,uBAAuB,EACvB,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,oBAAoB,EACpB,qBAAqB,GACrB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA"}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "engines": {
5
5
  "node": ">=20.3.0"
6
6
  },
7
- "version": "0.20.1",
7
+ "version": "0.20.2",
8
8
  "private": false,
9
9
  "description": "TypeScript SDK for ExpandAI",
10
10
  "author": "ExpandAI",
@@ -67,14 +67,17 @@
67
67
  "@durable-streams/client": "^0.2.6",
68
68
  "@effect/platform": "0.96.1",
69
69
  "@effect/platform-node": "0.106.0",
70
- "effect": "3.21.2"
70
+ "effect": "3.21.2",
71
+ "mime": "^4.1.0",
72
+ "ws": "^8.21.0"
71
73
  },
72
74
  "devDependencies": {
73
75
  "@biomejs/biome": "^2.4.14",
74
76
  "@effect/vitest": "0.29.0",
75
77
  "@tim-smart/openapi-gen": "0.3.12",
76
78
  "@types/node": "^25.5.2",
79
+ "@types/ws": "^8.18.1",
77
80
  "vitest": "4.1.4"
78
81
  },
79
- "expandSourceSha": "8d673e5359545b0494ca8396a90aa0006205b279"
82
+ "expandSourceSha": "71e04d6b697d70cdca5566e162dc4b9fdc98dba3"
80
83
  }
package/src/Error.ts CHANGED
@@ -1,5 +1,34 @@
1
1
  import { Schema } from 'effect'
2
2
 
3
+ const LOCAL_TARGET_ERROR_REASON = Schema.Literal(
4
+ 'InvalidInput',
5
+ 'NotLocal',
6
+ 'UnsupportedProtocol',
7
+ 'NotFound',
8
+ 'PermissionDenied',
9
+ 'UnsupportedFileType',
10
+ 'Unavailable',
11
+ 'UnsafeDirectory',
12
+ )
13
+
14
+ export type LocalTargetErrorReason = typeof LOCAL_TARGET_ERROR_REASON.Type
15
+
16
+ /** A local input cannot be fetched. Inspect `reason` rather than parsing the message. */
17
+ export class ExpandLocalTargetError extends Schema.TaggedError<ExpandLocalTargetError>()('ExpandLocalTargetError', {
18
+ reason: LOCAL_TARGET_ERROR_REASON,
19
+ input: Schema.String,
20
+ message: Schema.String,
21
+ cause: Schema.optional(Schema.Defect),
22
+ }) {
23
+ toPublicError() {
24
+ return new ExpandClientLocalTargetError(this.message, {
25
+ reason: this.reason,
26
+ input: this.input,
27
+ cause: this.cause,
28
+ })
29
+ }
30
+ }
31
+
3
32
  /**
4
33
  * Effect-side base error. Schema.TaggedError; Effect consumers receive these in the error channel.
5
34
  */
@@ -54,7 +83,12 @@ export class ExpandTimeoutError extends Schema.TaggedError<ExpandTimeoutError>()
54
83
  /**
55
84
  * Union of every error the Effect-side `ExpandService` can fail with.
56
85
  */
57
- export type ExpandError = ExpandSdkError | ExpandApiError | ExpandConnectionError | ExpandTimeoutError
86
+ export type ExpandError =
87
+ | ExpandSdkError
88
+ | ExpandApiError
89
+ | ExpandConnectionError
90
+ | ExpandTimeoutError
91
+ | ExpandLocalTargetError
58
92
 
59
93
  /**
60
94
  * Promise-side base class. Catch this to handle any failure thrown by `ExpandClient`.
@@ -66,6 +100,24 @@ export class ExpandClientError extends Error {
66
100
  }
67
101
  }
68
102
 
103
+ interface LocalTargetErrorOptions extends ErrorOptions {
104
+ readonly reason: LocalTargetErrorReason
105
+ readonly input: string
106
+ }
107
+
108
+ /** A local input cannot be fetched. Includes the input, stable reason, and original filesystem cause. */
109
+ export class ExpandClientLocalTargetError extends ExpandClientError {
110
+ readonly reason: LocalTargetErrorReason
111
+ readonly input: string
112
+
113
+ constructor(message: string, options: LocalTargetErrorOptions) {
114
+ super(message, options)
115
+ this.name = 'ExpandClientLocalTargetError'
116
+ this.reason = options.reason
117
+ this.input = options.input
118
+ }
119
+ }
120
+
69
121
  /**
70
122
  * Promise-side error for non-2xx API responses. Inspect `status` and `body` for details.
71
123
  */
package/src/Internal.ts CHANGED
@@ -5,9 +5,11 @@ import {
5
5
  ExpandClientApiError,
6
6
  ExpandClientConnectionError,
7
7
  ExpandClientError,
8
+ ExpandClientLocalTargetError,
8
9
  ExpandClientTimeoutError,
9
10
  ExpandConnectionError,
10
11
  type ExpandError,
12
+ ExpandLocalTargetError,
11
13
  ExpandSdkError,
12
14
  ExpandTimeoutError,
13
15
  } from './Error.js'
@@ -115,9 +117,17 @@ export function toExpandError(error: unknown, timeoutMs: number): ExpandError {
115
117
  error instanceof ExpandApiError ||
116
118
  error instanceof ExpandConnectionError ||
117
119
  error instanceof ExpandTimeoutError ||
120
+ error instanceof ExpandLocalTargetError ||
118
121
  error instanceof ExpandSdkError
119
122
  )
120
123
  return error
124
+ if (error instanceof ExpandClientLocalTargetError)
125
+ return new ExpandLocalTargetError({
126
+ reason: error.reason,
127
+ input: error.input,
128
+ message: error.message,
129
+ cause: error.cause,
130
+ })
121
131
  if (error instanceof ExpandClientApiError)
122
132
  return new ExpandApiError({ message: error.message, status: error.status, body: error.body, cause: error })
123
133
  if (error instanceof ExpandClientTimeoutError)
package/src/RetryAfter.ts CHANGED
@@ -25,6 +25,9 @@ export const MAX_RETRY_AFTER = Duration.minutes(2)
25
25
  */
26
26
  const currentRetryAfter = FiberRef.unsafeMake(Option.none<Duration.Duration>())
27
27
 
28
+ /** Start an attempt with no previous response's delay, including when setup fails before HTTP. */
29
+ export const clearRetryAfter = FiberRef.set(currentRetryAfter, Option.none())
30
+
28
31
  /**
29
32
  * Rounded to whole milliseconds rather than kept as fractional seconds.
30
33
  *
@@ -59,7 +62,7 @@ export const recordRetryAfter = <E, R>(client: HttpClient.HttpClient.With<E, R>)
59
62
  // transport level never produces a response to overwrite the previous one, so without this the
60
63
  // next attempt would sleep on a wait some earlier response asked for — a connection reset would
61
64
  // inherit a saturated gate's 20s.
62
- HttpClient.tapRequest(client, () => FiberRef.set(currentRetryAfter, Option.none())),
65
+ HttpClient.tapRequest(client, () => clearRetryAfter),
63
66
  (response) =>
64
67
  FiberRef.set(
65
68
  currentRetryAfter,
package/src/Service.ts CHANGED
@@ -10,7 +10,7 @@ import type {
10
10
  CrawlSyncPolicy,
11
11
  } from './Crawls.js'
12
12
  import * as EnvProxyHttpClient from './EnvProxyHttpClient.js'
13
- import { ExpandApiError, type ExpandError, ExpandSdkError } from './Error.js'
13
+ import { ExpandApiError, ExpandConnectionError, type ExpandError, ExpandSdkError } from './Error.js'
14
14
  import {
15
15
  type FetchObjectModeJsonResponse,
16
16
  type FetchSnapshotHighlightsResultWithJson,
@@ -20,7 +20,8 @@ import {
20
20
  import * as Generated from './Generated.js'
21
21
  import { decodeError, unexpectedStatus } from './HttpClientHelpers.js'
22
22
  import { isRetryable, toExpandError } from './Internal.js'
23
- import { nextRetryDelay, recordRetryAfter } from './RetryAfter.js'
23
+ import { clearRetryAfter, nextRetryDelay, recordRetryAfter } from './RetryAfter.js'
24
+ import { TunnelError, withLocalTunnel } from './Tunnel/Tunnel.js'
24
25
 
25
26
  const DEFAULT_BASE_URL = 'https://api.expand.ai'
26
27
  const DEFAULT_TIMEOUT_MS = 60_000
@@ -79,15 +80,23 @@ export class RequestOptions extends Schema.Class<RequestOptions>('RequestOptions
79
80
  { description: 'Per-request overrides for an `ExpandService` operation.' },
80
81
  ) {}
81
82
 
83
+ interface LocalOptions {
84
+ /**
85
+ * The URL is a localhost server or a file path on this machine: fetch it through a short-lived tunnel
86
+ * (Node.js only). Never set this for user-supplied URLs; it exposes this machine's loopback and files.
87
+ */
88
+ readonly local?: boolean
89
+ }
90
+
82
91
  export type FetchParams = typeof Generated.FetchFetchRequest.Encoded
83
92
  export type FetchResponse = string
84
- export type FetchRequestOptions = RequestOptions & typeof Generated.FetchFetchParams.Encoded
93
+ export type FetchRequestOptions = RequestOptions & typeof Generated.FetchFetchParams.Encoded & LocalOptions
85
94
  export type FetchJsonParams = typeof Generated.FetchFetchJsonRequest.Encoded
86
95
  // `*WithJson` rather than the generated result: the codegen drops the statejson snippet `json`, so
87
96
  // the service decodes the body with the recovering schemas (see `FetchJson.ts`). A union because an
88
97
  // origin that never answered has no content to describe — narrow on `_tag`.
89
98
  export type FetchJsonResponse = FetchObjectModeJsonResponse
90
- export type FetchJsonRequestOptions = RequestOptions & typeof Generated.FetchFetchJsonParams.Encoded
99
+ export type FetchJsonRequestOptions = RequestOptions & typeof Generated.FetchFetchJsonParams.Encoded & LocalOptions
91
100
  export interface SnapshotHighlightsParams extends Schema.Schema.Encoded<typeof Generated.SnapshotHighlightsInput> {
92
101
  readonly snapshotId: string
93
102
  }
@@ -257,6 +266,7 @@ export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sd
257
266
  ),
258
267
  )
259
268
  return yield* Effect.sync(() => MutableRef.set(pendingRefusal, Option.none())).pipe(
269
+ Effect.zipRight(clearRetryAfter),
260
270
  Effect.zipRight(operation),
261
271
  Effect.retry(retrySchedule),
262
272
  Effect.timeoutFail({
@@ -268,23 +278,53 @@ export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sd
268
278
  )
269
279
  })
270
280
 
281
+ // Tunnel setup and local access belong to the attempt covered by the existing retry/timeout policy.
282
+ function viaLocalTunnel<A, E, R>(
283
+ url: string,
284
+ local: boolean | undefined,
285
+ run: (url: string) => Effect.Effect<A, E, R>,
286
+ ) {
287
+ return local === true
288
+ ? withLocalTunnel(
289
+ url,
290
+ Effect.succeed({ baseUrl: config.baseUrl, headers: { 'x-expand-api-key': config.apiKey } }),
291
+ run,
292
+ ).pipe(
293
+ Effect.mapError((error) => {
294
+ if (!(error instanceof TunnelError)) return error
295
+ return error.reason === 'RelayUnreachable'
296
+ ? new ExpandConnectionError({ message: error.message, cause: error })
297
+ : new ExpandSdkError({ message: error.message, cause: error })
298
+ }),
299
+ )
300
+ : run(url)
301
+ }
302
+
271
303
  const fetch = Effect.fn('ExpandService.fetch')(function* (
272
304
  params: FetchParams,
273
305
  requestOptions: FetchRequestOptions = {},
274
306
  ) {
275
- const { include, ...retryOptions } = requestOptions
276
- return yield* withRetries(fetchMarkdown(params, { include }), retryOptions)
307
+ const { include, local, ...retryOptions } = requestOptions
308
+ return yield* withRetries(
309
+ viaLocalTunnel(params.url, local, (url) => fetchMarkdown({ ...params, url }, { include })),
310
+ retryOptions,
311
+ )
277
312
  })
278
313
 
279
314
  const fetchJson = Effect.fn('ExpandService.fetchJson')(function* (
280
315
  params: FetchJsonParams,
281
316
  requestOptions: FetchJsonRequestOptions = {},
282
317
  ) {
283
- const { include, ...retryOptions } = requestOptions
318
+ const { include, local, ...retryOptions } = requestOptions
284
319
  // `fetchObjectModeJson` mirrors the generated call but preserves the statejson snippet `json`.
285
320
  // Coalesce null -> undefined before `setUrlParams`, which throws on null.
286
321
  return yield* withRetries(
287
- fetchObjectModeJson(authedClient, { params: { include: include ?? undefined }, payload: params }),
322
+ viaLocalTunnel(params.url, local, (url) =>
323
+ fetchObjectModeJson(authedClient, {
324
+ params: { include: include ?? undefined },
325
+ payload: { ...params, url },
326
+ }),
327
+ ),
288
328
  retryOptions,
289
329
  )
290
330
  })
@@ -0,0 +1,22 @@
1
+ const HOP_BY_HOP_HEADERS = new Set([
2
+ 'connection',
3
+ 'keep-alive',
4
+ 'proxy-authenticate',
5
+ 'proxy-authorization',
6
+ 'te',
7
+ 'trailer',
8
+ 'transfer-encoding',
9
+ 'upgrade',
10
+ ])
11
+
12
+ export function endToEndHeaders(headers: Iterable<readonly [string, string]>): Array<readonly [string, string]> {
13
+ const entries = [...headers]
14
+ const connectionTokens = new Set(
15
+ entries
16
+ .filter(([name]) => name.toLowerCase() === 'connection')
17
+ .flatMap(([, value]) => value.split(',').map((token) => token.trim().toLowerCase())),
18
+ )
19
+ return entries.filter(
20
+ ([name]) => !HOP_BY_HOP_HEADERS.has(name.toLowerCase()) && !connectionTokens.has(name.toLowerCase()),
21
+ )
22
+ }