@expandai/sdk 0.19.0 → 0.20.0

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 (72) hide show
  1. package/README.md +130 -4
  2. package/build/dts/AppendixMarkers.d.ts +30 -0
  3. package/build/dts/AppendixMarkers.d.ts.map +1 -0
  4. package/build/dts/Client.d.ts +41 -6
  5. package/build/dts/Client.d.ts.map +1 -1
  6. package/build/dts/Crawls.d.ts +568 -0
  7. package/build/dts/Crawls.d.ts.map +1 -0
  8. package/build/dts/EnvProxyHttpClient.d.ts +28 -0
  9. package/build/dts/EnvProxyHttpClient.d.ts.map +1 -0
  10. package/build/dts/FetchJson.d.ts +444 -37
  11. package/build/dts/FetchJson.d.ts.map +1 -1
  12. package/build/dts/Generated.d.ts +1545 -788
  13. package/build/dts/Generated.d.ts.map +1 -1
  14. package/build/dts/HttpClientHelpers.d.ts +13 -0
  15. package/build/dts/HttpClientHelpers.d.ts.map +1 -1
  16. package/build/dts/Internal.d.ts +6 -4
  17. package/build/dts/Internal.d.ts.map +1 -1
  18. package/build/dts/ParseErrorBody.d.ts +26 -0
  19. package/build/dts/ParseErrorBody.d.ts.map +1 -0
  20. package/build/dts/Playground.d.ts +20 -0
  21. package/build/dts/Playground.d.ts.map +1 -1
  22. package/build/dts/RetryAfter.d.ts +40 -0
  23. package/build/dts/RetryAfter.d.ts.map +1 -0
  24. package/build/dts/Service.d.ts +94 -45
  25. package/build/dts/Service.d.ts.map +1 -1
  26. package/build/dts/StreamPage.d.ts +10 -0
  27. package/build/dts/StreamPage.d.ts.map +1 -0
  28. package/build/dts/index.d.ts +9 -2
  29. package/build/dts/index.d.ts.map +1 -1
  30. package/build/esm/AppendixMarkers.js +307 -0
  31. package/build/esm/AppendixMarkers.js.map +1 -0
  32. package/build/esm/Client.js +96 -15
  33. package/build/esm/Client.js.map +1 -1
  34. package/build/esm/Crawls.js +786 -0
  35. package/build/esm/Crawls.js.map +1 -0
  36. package/build/esm/EnvProxyHttpClient.js +55 -0
  37. package/build/esm/EnvProxyHttpClient.js.map +1 -0
  38. package/build/esm/FetchJson.js +48 -22
  39. package/build/esm/FetchJson.js.map +1 -1
  40. package/build/esm/Generated.js +540 -112
  41. package/build/esm/Generated.js.map +1 -1
  42. package/build/esm/HttpClientHelpers.js +36 -1
  43. package/build/esm/HttpClientHelpers.js.map +1 -1
  44. package/build/esm/Internal.js +140 -33
  45. package/build/esm/Internal.js.map +1 -1
  46. package/build/esm/ParseErrorBody.js +41 -0
  47. package/build/esm/ParseErrorBody.js.map +1 -0
  48. package/build/esm/Playground.js +38 -0
  49. package/build/esm/Playground.js.map +1 -1
  50. package/build/esm/RetryAfter.js +104 -0
  51. package/build/esm/RetryAfter.js.map +1 -0
  52. package/build/esm/Service.js +96 -17
  53. package/build/esm/Service.js.map +1 -1
  54. package/build/esm/StreamPage.js +46 -0
  55. package/build/esm/StreamPage.js.map +1 -0
  56. package/build/esm/index.js +5 -1
  57. package/build/esm/index.js.map +1 -1
  58. package/package.json +8 -2
  59. package/src/AppendixMarkers.ts +370 -0
  60. package/src/Client.ts +131 -16
  61. package/src/Crawls.ts +1387 -0
  62. package/src/EnvProxyHttpClient.ts +71 -0
  63. package/src/FetchJson.ts +63 -25
  64. package/src/Generated.ts +738 -162
  65. package/src/HttpClientHelpers.ts +48 -1
  66. package/src/Internal.ts +163 -105
  67. package/src/ParseErrorBody.ts +42 -0
  68. package/src/Playground.ts +48 -0
  69. package/src/RetryAfter.ts +138 -0
  70. package/src/Service.ts +208 -25
  71. package/src/StreamPage.ts +47 -0
  72. package/src/index.ts +29 -3
@@ -0,0 +1,71 @@
1
+ import type { HttpClient } from '@effect/platform'
2
+ import { NodeHttpClient, Undici } from '@effect/platform-node'
3
+ import { Effect, Layer, Option } from 'effect'
4
+ import { ExpandSdkError } from './Error.js'
5
+
6
+ // Undici resolves each protocol's proxy as `lowercase ?? UPPERCASE` — the
7
+ // lowercase variant wins even when empty, and an empty effective value means
8
+ // "no proxy". `NO_PROXY` never fails to parse, so it is not checked.
9
+ const PROXY_ENV_SOURCES: ReadonlyArray<readonly [string, string]> = [
10
+ ['http_proxy', 'HTTP_PROXY'],
11
+ ['https_proxy', 'HTTPS_PROXY'],
12
+ ]
13
+
14
+ /**
15
+ * Describes the first configured proxy environment variable whose value is not
16
+ * a valid URL, or `None` when the proxy configuration is usable. Undici's
17
+ * `EnvHttpProxyAgent` throws a raw `TypeError: Invalid URL` mid-construction
18
+ * on such a value; callers surface this message early instead (the SDK's
19
+ * `buildServiceConfig`, the CLI's startup preflight). Checked with the same
20
+ * effective-value precedence undici applies, so a value undici would never
21
+ * parse is never rejected here.
22
+ */
23
+ export function invalidProxyEnvMessage(): Option.Option<string> {
24
+ for (const [lowercase, uppercase] of PROXY_ENV_SOURCES) {
25
+ const name = process.env[lowercase] !== undefined ? lowercase : uppercase
26
+ const value = process.env[name]
27
+ if (value === undefined || value === '') {
28
+ continue
29
+ }
30
+ try {
31
+ new URL(value)
32
+ } catch {
33
+ // Mirrors undici: any value `new URL` rejects would crash EnvHttpProxyAgent.
34
+ return Option.some(
35
+ `Invalid proxy URL in the ${name} environment variable: ${JSON.stringify(value)}. Fix or unset it.`,
36
+ )
37
+ }
38
+ }
39
+ return Option.none()
40
+ }
41
+
42
+ /**
43
+ * `HttpClient` layer honoring the standard proxy environment variables
44
+ * `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` (upper- or lowercase).
45
+ *
46
+ * Requests dispatch through undici's `EnvHttpProxyAgent`, which resolves the
47
+ * proxy configuration at acquisition and degrades to a plain undici `Agent`
48
+ * when no proxy variable is set.
49
+ *
50
+ * A malformed proxy URL dies with a descriptive `ExpandSdkError` rather than
51
+ * failing typed: an error channel here would ripple through every dependent
52
+ * service's `Default` layer type, and the callers that can act on the problem
53
+ * already validate eagerly via {@link invalidProxyEnvMessage}. The die is the
54
+ * safety net for paths that skip that, e.g. the CLI's exported MCP server.
55
+ */
56
+ export const layer: Layer.Layer<HttpClient.HttpClient> = NodeHttpClient.layerUndiciWithoutDispatcher.pipe(
57
+ Layer.provide(
58
+ Layer.scoped(
59
+ NodeHttpClient.Dispatcher,
60
+ Effect.acquireRelease(
61
+ Effect.suspend(() =>
62
+ Option.match(invalidProxyEnvMessage(), {
63
+ onSome: (message) => Effect.die(new ExpandSdkError({ message })),
64
+ onNone: () => Effect.sync(() => new Undici.EnvHttpProxyAgent()),
65
+ }),
66
+ ),
67
+ (dispatcher) => Effect.promise(() => dispatcher.destroy()),
68
+ ),
69
+ ),
70
+ ),
71
+ )
package/src/FetchJson.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // The OpenAPI spec declares `json` on each search snippet (the extracted State JSON value), but
4
4
  // the code generator drops the bare `unknown`-typed field, so `Generated.FetchObjectModeResult`
5
- // / `Generated.FetchSnapshotSearchResult` decode it away. These hand-written schemas re-add
5
+ // / `Generated.FetchSnapshotHighlightsResult` decode it away. These hand-written schemas re-add
6
6
  // `json`, and the request functions mirror the generated client but decode the 200 body with
7
7
  // them — so callers that need to surface the cited JSON get it. Kept out of `Generated.ts` so the
8
8
  // OpenAPI conformance test (which checks the generated schemas) is unaffected.
@@ -10,38 +10,69 @@
10
10
  import { HttpClient, HttpClientRequest, HttpClientResponse } from '@effect/platform'
11
11
  import { Effect, Schema as S } from 'effect'
12
12
  import * as Generated from './Generated.js'
13
- import { decodeError, unexpectedStatus } from './HttpClientHelpers.js'
13
+ import { decodeError, decodeSuccess, unexpectedStatus } from './HttpClientHelpers.js'
14
14
 
15
15
  /** Search snippet with the extracted `json` value preserved (statejson snippets only). */
16
- export const FetchSearchSnippetWithJson = S.Struct({
17
- ...Generated.FetchSearchResult.fields.snippets.value.fields,
16
+ export const FetchHighlightsSnippetWithJson = S.Struct({
17
+ ...Generated.FetchHighlightsResult.fields.snippets.value.fields,
18
18
  json: S.optionalWith(S.Unknown, { nullable: true }),
19
19
  })
20
20
 
21
- export const FetchSearchResultWithJson = S.Struct({
22
- ...Generated.FetchSearchResult.fields,
23
- snippets: S.Array(FetchSearchSnippetWithJson),
21
+ export const FetchHighlightsResultWithJson = S.Struct({
22
+ ...Generated.FetchHighlightsResult.fields,
23
+ snippets: S.Array(FetchHighlightsSnippetWithJson),
24
+ })
25
+
26
+ /** Object-mode evidence with the inline JSON payload preserved. */
27
+ export const FetchObjectModeEvidenceWithJson = S.Struct({
28
+ ...Generated.FetchObjectModeEvidence.fields,
29
+ body: S.optionalWith(S.Unknown, { nullable: true }),
24
30
  })
25
31
 
26
32
  export class FetchObjectModeResultWithJson extends S.Class<FetchObjectModeResultWithJson>(
27
33
  'FetchObjectModeResultWithJson',
28
34
  )({
29
35
  ...Generated.FetchObjectModeResult.fields,
36
+ json: S.Array(FetchObjectModeEvidenceWithJson),
30
37
  data: S.optionalWith(
31
38
  S.Struct({
32
39
  ...Generated.FetchObjectModeResult.fields.data.from.fields,
33
- search: S.optionalWith(FetchSearchResultWithJson, { nullable: true }),
40
+ highlights: S.optionalWith(FetchHighlightsResultWithJson, { nullable: true }),
34
41
  }),
35
42
  { nullable: true },
36
43
  ),
44
+ originError: S.optionalWith(Generated.FetchHttpOriginError, { nullable: true }),
37
45
  }) {}
38
46
 
39
- export class FetchSnapshotSearchResultWithJson extends S.Class<FetchSnapshotSearchResultWithJson>(
40
- 'FetchSnapshotSearchResultWithJson',
47
+ /**
48
+ * The 200 body of `/v1/fetch/json`. An origin that never answered has no
49
+ * content to return, so the endpoint reports its outcome as the whole body
50
+ * rather than as an HTTP error status — a non-2xx there would mean Expand
51
+ * failed, which it did not.
52
+ *
53
+ * Discriminate on `_tag`, which only `FetchOriginError` carries.
54
+ */
55
+ export const FetchObjectModeJsonResponse = S.Union(
56
+ FetchObjectModeResultWithJson,
57
+ Generated.FetchNetworkOriginError,
58
+ Generated.FetchHttpOriginRefusal,
59
+ )
60
+ export type FetchObjectModeJsonResponse = typeof FetchObjectModeJsonResponse.Type
61
+
62
+ /** The unreachable-origin member of `FetchObjectModeJsonResponse`, after narrowing with `isFetchOriginError`. */
63
+ export type FetchOriginErrorResponse = Extract<FetchObjectModeJsonResponse, { readonly _tag: 'FetchOriginError' }>
64
+
65
+ /** Narrows a `FetchObjectModeJsonResponse` to the unreachable-origin case — the only variant with a `_tag`. */
66
+ export function isFetchOriginError(response: FetchObjectModeJsonResponse): response is FetchOriginErrorResponse {
67
+ return '_tag' in response && response._tag === 'FetchOriginError'
68
+ }
69
+
70
+ export class FetchSnapshotHighlightsResultWithJson extends S.Class<FetchSnapshotHighlightsResultWithJson>(
71
+ 'FetchSnapshotHighlightsResultWithJson',
41
72
  )({
42
73
  snapshotId: S.String.pipe(S.minLength(1)),
43
74
  response: Generated.ResponseInfo,
44
- search: FetchSearchResultWithJson,
75
+ highlights: FetchHighlightsResultWithJson,
45
76
  durationMs: S.Number.pipe(S.greaterThanOrEqualTo(0)),
46
77
  playground: S.String,
47
78
  }) {}
@@ -61,15 +92,21 @@ export const fetchObjectModeJson = (
61
92
  Effect.flatMap(
62
93
  httpClient.execute(request),
63
94
  HttpClientResponse.matchStatus({
64
- '200': (response) => HttpClientResponse.schemaBodyJson(FetchObjectModeResultWithJson)(response),
95
+ // `decodeSuccess`, not a raw schema decode: a 2xx body this SDK cannot read is
96
+ // deterministic skew, and retrying it re-runs a full capture per attempt.
97
+ '200': (response) => decodeSuccess(response, FetchObjectModeJsonResponse),
65
98
  '400': (response) => decodeError(response, Generated.FetchFetchJson400),
66
99
  '401': (response) => decodeError(response, Generated.AuthFailed),
67
100
  '403': (response) => decodeError(response, Generated.FetchFetchJson403),
68
- '413': (response) => decodeError(response, Generated.PayloadTooLarge),
69
- '429': (response) => decodeError(response, Generated.TooManyRequests),
101
+ '404': (response) => decodeError(response, Generated.FetchOriginNotFound),
102
+ '410': (response) => decodeError(response, Generated.FetchOriginGone),
103
+ '413': (response) => decodeError(response, Generated.FetchFetchJson413),
104
+ '415': (response) => decodeError(response, Generated.FetchUnsupportedContentType),
105
+ '422': (response) => decodeError(response, Generated.FetchVideoUnavailable),
106
+ '429': (response) => decodeError(response, Generated.FetchFetchJson429),
70
107
  '500': (response) => decodeError(response, Generated.InternalError),
71
- '502': (response) => decodeError(response, Generated.FetchNavigationFailed),
72
- '503': (response) => decodeError(response, Generated.ServiceUnavailable),
108
+ '502': (response) => decodeError(response, Generated.FetchFetchJson502),
109
+ '503': (response) => decodeError(response, Generated.FetchFetchJson503),
73
110
  '504': (response) => decodeError(response, Generated.FetchCaptureTimeout),
74
111
  '529': (response) => decodeError(response, Generated.FetchCapacityTimeout),
75
112
  orElse: (response) => unexpectedStatus(request, response),
@@ -78,24 +115,25 @@ export const fetchObjectModeJson = (
78
115
  ),
79
116
  )
80
117
 
81
- /** Mirrors `Generated.Client['fetchFetchSearch']` but decodes the 200 body with `json` preserved. */
82
- export const fetchSnapshotSearch = (
118
+ /** Mirrors `Generated.Client['snapshotsHighlights']` but decodes the 200 body with `json` preserved. */
119
+ export const fetchSnapshotHighlights = (
83
120
  httpClient: HttpClient.HttpClient,
84
- options: typeof Generated.FetchFetchSearchRequest.Encoded,
121
+ snapshotId: string,
122
+ options: typeof Generated.SnapshotHighlightsInput.Encoded,
85
123
  ) =>
86
- HttpClientRequest.make('POST')('/v1/fetch/search').pipe(
124
+ HttpClientRequest.make('POST')(`/v1/snapshots/${encodeURIComponent(snapshotId)}/highlights`).pipe(
87
125
  (request) => Effect.orDie(HttpClientRequest.bodyJson(request, options)),
88
126
  Effect.flatMap((request) =>
89
127
  Effect.flatMap(
90
128
  httpClient.execute(request),
91
129
  HttpClientResponse.matchStatus({
92
- '200': (response) => HttpClientResponse.schemaBodyJson(FetchSnapshotSearchResultWithJson)(response),
93
- '400': (response) => decodeError(response, Generated.FetchFetchSearch400),
130
+ '200': (response) => decodeSuccess(response, FetchSnapshotHighlightsResultWithJson),
131
+ '400': (response) => decodeError(response, Generated.SnapshotsHighlights400),
94
132
  '401': (response) => decodeError(response, Generated.AuthFailed),
95
- '403': (response) => decodeError(response, Generated.FetchFetchSearch403),
96
- '404': (response) => decodeError(response, Generated.FetchSnapshotSearchNotFound),
133
+ '403': (response) => decodeError(response, Generated.SnapshotsHighlights403),
134
+ '404': (response) => decodeError(response, Generated.FetchSnapshotHighlightsNotFound),
97
135
  '413': (response) => decodeError(response, Generated.PayloadTooLarge),
98
- '429': (response) => decodeError(response, Generated.TooManyRequests),
136
+ '429': (response) => decodeError(response, Generated.SnapshotsHighlights429),
99
137
  '500': (response) => decodeError(response, Generated.InternalError),
100
138
  '503': (response) => decodeError(response, Generated.ServiceUnavailable),
101
139
  orElse: (response) => unexpectedStatus(request, response),