@superdurable/connectors-sdk 0.1.0 → 0.2.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.
- package/README.md +27 -3
- package/dist/{chunk-2NBM7P4T.js → chunk-32YJXBE4.js} +1 -1
- package/dist/chunk-466BPS3M.js +336 -0
- package/dist/{chunk-IVJLNZVL.js → chunk-BG65RPGH.js} +70 -15
- package/dist/{chunk-SOXYPIMO.js → chunk-BQGYGCOQ.js} +35 -73
- package/dist/chunk-IHG6ADQG.js +44 -0
- package/dist/chunk-NQQJS6OC.js +316 -0
- package/dist/{chunk-KD5DYYSU.js → chunk-QDMBAWXD.js} +1 -1
- package/dist/connectormock/index.d.ts +3 -2
- package/dist/connectormock/index.js +5 -3
- package/dist/credentials-JqG97kJ5.d.ts +102 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +19 -291
- package/dist/oauthtoken/index.d.ts +36 -2
- package/dist/oauthtoken/index.js +326 -5
- package/dist/{operation-Ca8rfacf.d.ts → operation-CASLOg86.d.ts} +2 -1
- package/dist/projectconfig/index.d.ts +2 -1
- package/dist/projectconfig/index.js +5 -3
- package/dist/providerhttp/index.d.ts +21 -4
- package/dist/providerhttp/index.js +3 -1
- package/dist/query-CeUaW9xU.d.ts +42 -0
- package/dist/singledispatch/index.d.ts +2 -1
- package/dist/singledispatch/index.js +2 -1
- package/dist/textgen/index.d.ts +15 -0
- package/dist/textgen/index.js +628 -0
- package/dist/textgen/openaichat/index.d.ts +44 -0
- package/dist/textgen/openaichat/index.js +232 -0
- package/dist/textgen/textgentest/index.d.ts +106 -0
- package/dist/textgen/textgentest/index.js +537 -0
- package/dist/{credentials-vrvW0d4j.d.ts → types-D6De3sry.d.ts} +1 -100
- package/dist/wire_format-stEVruMs.d.ts +190 -0
- package/package.json +14 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Connector TypeScript SDK
|
|
2
2
|
|
|
3
|
-
`@superdurable/connectors-sdk` is the provider-neutral TypeScript SDK for Dex connectors. It provides stable value contracts, typed Query and Mutation operations, Dex Step factories, credential refresh, OAuth token exchanges, HTTP safety helpers, project configuration loading, scripted mocks, and single-dispatch marker decisions. The package is at version `0.
|
|
3
|
+
`@superdurable/connectors-sdk` is the provider-neutral TypeScript SDK for Dex connectors. It provides stable value contracts, typed Query and Mutation operations, Dex Step factories, credential refresh, OAuth token exchanges, HTTP safety helpers, project configuration loading, scripted mocks, and single-dispatch marker decisions. The package is at version `0.2.0`, requires Node 22 or 24, and uses `@superdurable/dex` 1.28.0 or newer. It is ESM-only, matching Dex: use `import`; direct `require()` fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`.
|
|
4
4
|
|
|
5
5
|
Install dependencies and verify the package from this directory:
|
|
6
6
|
|
|
@@ -17,14 +17,32 @@ node examples/refreshing_project.mjs
|
|
|
17
17
|
node examples/shared_project.mjs
|
|
18
18
|
node examples/connectormock.mjs
|
|
19
19
|
node examples/oauthtoken.mjs
|
|
20
|
+
node examples/manifest_refresh.mjs
|
|
20
21
|
node examples/singledispatch.mjs
|
|
21
22
|
node examples/fence.mjs
|
|
23
|
+
node examples/textgen.mjs
|
|
22
24
|
```
|
|
23
25
|
|
|
24
26
|
All durations in this package are milliseconds. Public JSON shapes use the Go SDK's field names. Credential values and project objects stay outside Flow state and cannot be serialized through their wrappers.
|
|
25
27
|
|
|
26
28
|
The [Dex integration harness](test/integ/step_flows.mjs) exercises the same Step factories through a real Worker and Client. Start `dexcli dev --open=false --dex-port 18891` with a local Dex server, set `DEX_SERVER_ADDRESS` when its gRPC address differs from `127.0.0.1:18891`, and run `npm run test:integration`. It verifies a happy path, optional branch, unwired optional branch failure, retry then success, retry exhaustion, uncertain mutation, a lost-response single-dispatch fence, repeated claim heartbeats, result Attribute persistence, progress and text Stream delivery, and scripted mock replay. It uses fixture operations and no provider credentials. CI runs the same harness in `sdkts-integration` using its once-per-run resolved Dex CLI.
|
|
27
29
|
|
|
30
|
+
## Portable text generation
|
|
31
|
+
|
|
32
|
+
The [runnable text generation example](examples/textgen.mjs) uses `newTextGenerationQuery` with the OpenAI-compatible Chat Completions adapter from `@superdurable/connectors-sdk/textgen/openaichat`. The `@superdurable/connectors-sdk/textgen` entrypoint also exports request and response contracts, model ID validation, portable schema checks, branch definitions, temperature policies, error rules, and the provider-neutral wire-format contract. The example exercises a local fixture provider; a consuming connector supplies a real connection, credential resolver, and Dex Step.
|
|
33
|
+
|
|
34
|
+
`generateText` requires sync Execute durability and a request timeout shorter than the Step Execute timeout. It writes streamed text deltas to a configured text Stream as bytes arrive; the Result text is authoritative after retries or a blocked response. Register that Stream in the Flow persistence schema. Its six branches are `generated`, `truncated`, `blocked`, `providerRejected`, `invalidResponse`, and `defect`; only `generated` is required. The query retries transport errors, stalled responses, interrupted streams, and safe HTTP failures. A credential is resolved once after local validation and is never stored in Flow state, results, failures, or receipts.
|
|
35
|
+
|
|
36
|
+
Each invocation resolves the trimmed request model or connection model, then validates messages, the token limit, the structured-output name and schema, declared features, temperature, and reasoning effort in that order. It applies model schema rules, resolves one credential, and encodes the request before dispatch. The first error in this order selects `defect` with zero provider requests. A nil heartbeat every five seconds keeps a silent call alive and clears any Step heartbeat checkpoint. Stream text may contain text from an attempt later retried or blocked, so consumers use the final Result for decisions. A caller may inject `httpClient` to own HTTP transport as in Go; the default transport permits private HTTPS endpoints and neither transport should replay redirects.
|
|
37
|
+
|
|
38
|
+
An injected HTTP client may return a complete SSE response body without calling `onResponseChunk`. If it calls `onResponseChunk`, it must feed every chunk in order before returning and stop when the callback returns `false`. A provider retry-delay reader that throws or returns a non-finite value is treated as absent, allowing the `Retry-After` header to supply the delay. Usage token counts are JavaScript numbers and require safe integers; unlike Go's `int64` counts, larger values are rejected because they cannot be represented exactly in the public TypeScript `Usage` contract.
|
|
39
|
+
|
|
40
|
+
The schema subset is the same as Go `sdkgo/textgen`: root object, closed objects with every property required, nullable optional values, arrays with `items`, and only the documented portable keywords. Structured output is checked locally even when the provider accepts a transformed schema. Model IDs are validated according to whether the adapter sends them in a JSON body or path segment. Optional request fields rejected by a wire format or selected model produce a `defect` branch before dispatch.
|
|
41
|
+
|
|
42
|
+
Exact structured-output numbers may use at most 256 literal characters and an exponent magnitude of at most 400. Date and time formats follow Go's parser, including calendar-valid dates, one-digit hours, comma fractions, and offsets through hour 24 and minute 60. The JSON-object instruction uses Go's sorted, HTML-safe JSON encoding so provider requests match the Go connector. The shared [string-format fixture](../sdkgo/textgen/textgentest/testdata/string_format_cases.json) runs in both Go and TypeScript tests.
|
|
43
|
+
|
|
44
|
+
The `@superdurable/connectors-sdk/textgen/textgentest` entrypoint ports Go's credential-safe fake provider and reusable exchange suite. [Its provider-dialect test](test/textgentest.test.ts) exercises model precedence, all common branches, error rules, complete and interrupted streams, usage, receipts, and credential redaction. The [Dex integration harness](test/integ/step_flows.mjs) calls `runTextGenerationDexScenarios` through a real Worker and Client to verify retry, heartbeat, text Stream persistence, and an unwired optional branch.
|
|
45
|
+
|
|
28
46
|
## Typed operations and credential resolution
|
|
29
47
|
|
|
30
48
|
The [runnable operation example](examples/operations.mjs) validates a Query definition, binds a live Step context to stable call identity, resolves credentials at invocation time, and completes a Go-compatible zero receipt. `Mutation` adds an idempotency-key method and may return `newMutationUncertain` only for a dispatched write that cannot be repeated or confirmed by read-back. `runQuery` and `runMutation` throw `RetryError` for a safe retry; a Step factory turns that into Dex retry behavior. The example uses a test context; application Steps receive `AsyncContext` from Dex.
|
|
@@ -99,7 +117,7 @@ if (result.kind !== 'branch' || retryPolicy.totalDurationMs !== 3600000 || crede
|
|
|
99
117
|
|
|
100
118
|
## Provider HTTP
|
|
101
119
|
|
|
102
|
-
The [complete runnable HTTP example](examples/providerhttp.mjs) sends one bounded request to a local server. `sendProviderRequest` resolves every address before connecting, pins the chosen address, and returns redirects without following them. `ProviderTransportError.isProvablyNotSent` is true
|
|
120
|
+
The [complete runnable HTTP example](examples/providerhttp.mjs) sends one bounded request to a local server. `sendProviderRequest` resolves every address before connecting, pins the chosen address for IP literals and hostnames, and returns redirects without following them. `ProviderTransportError.isProvablyNotSent` is true for DNS, TCP connect, or TLS handshake failures before a usable connection could send HTTP bytes. Failures after connection or request writing have an unknown dispatch outcome, matching the Go connectors' DNS, connect, TLS, and obtained-connection trace.
|
|
103
121
|
`BodyTooLargeError` preserves the response `status` and `headers`, so a caller can classify a 429 or 503 and its `Retry-After` even when the response body exceeds its limit.
|
|
104
122
|
|
|
105
123
|
```js
|
|
@@ -212,7 +230,9 @@ mutation.assertNoFailures();
|
|
|
212
230
|
|
|
213
231
|
## OAuth token refresh
|
|
214
232
|
|
|
215
|
-
The [runnable OAuth exchange example](examples/oauthtoken.mjs) shows a provider-neutral refresh-token grant. The injected `fetch` returns a local fixture; a connector normally uses the provider's fixed HTTPS token endpoint. `TokenEndpoint` also supports client-credentials and JWT-bearer grants, optional Basic authentication, declared response fields, bounded responses, and terminal OAuth error-code classification.
|
|
233
|
+
The [runnable OAuth exchange example](examples/oauthtoken.mjs) shows a provider-neutral refresh-token grant. The injected `fetch` returns a local fixture; a connector normally uses the provider's fixed HTTPS token endpoint. `TokenEndpoint` also supports client-credentials and JWT-bearer grants, optional Basic authentication, declared response fields, bounded responses, and terminal OAuth error-code classification. The [manifest refresh example](examples/manifest_refresh.mjs) uses `ManifestCredentialRefreshDriver` with a generated authorization declaration and field map to exchange refresh-token, client-credentials, or JWT-bearer grants, preserve rotated material, check scopes, and replace only mapped fields. Constructor options also accept startup-pinned `settings`, a caller-owned `fetch`, and an injectable `now` clock. Credentials for a connector with several authorization methods must carry `authMethodId`. The manifest must declare `refresh.acceptsMissingTokenType` for providers that omit `token_type` and grant `acceptedTokenTypes` for providers such as Slack that return `bot` or `user`; schema and codegen support for these declarations is pending before connector migration. Only declared terminal OAuth errors below HTTP 500 require reauthorization; transport, server, and other token errors remain retryable.
|
|
234
|
+
|
|
235
|
+
An access token with no stored expiry remains usable; the current project provider requires authoritative expiry before forcing refresh after a provider rejection. A response without `expires_in` uses the manifest's `missingExpiry` lifetime or returns an expired result for `keep`. The current project provider rejects that result without changing stored material. Refreshing multiple grants follows the Go driver's current single-expiry behavior: active grants rotate together and the earliest expiry wins. A process failure between provider response and persistence, or failure of a later grant after an earlier rotation, can lose a one-use refresh token.
|
|
216
236
|
|
|
217
237
|
```js
|
|
218
238
|
import { SecretString } from '../dist/index.js';
|
|
@@ -260,6 +280,10 @@ await fence.recordProviderId(result.providerId);
|
|
|
260
280
|
|
|
261
281
|
## Release notes
|
|
262
282
|
|
|
283
|
+
### 0.2.0
|
|
284
|
+
|
|
285
|
+
Adds manifest-driven refresh for OAuth, client-credentials, and JWT bearer grants, including rotated refresh tokens, declared scope and token-type checks, and retryable token endpoint failures. Connector migration requires schema and codegen support for the new token-type declarations.
|
|
286
|
+
|
|
263
287
|
### 0.1.0
|
|
264
288
|
|
|
265
289
|
First public ESM-only TypeScript Connector SDK release: Go-compatible contracts and conformance fixtures, safe provider HTTP, project configuration with durable credential refresh and guarded local writeback, typed operations and Dex Step factories, OAuth exchanges, scripted mocks, and the live single-dispatch fence. No live provider authorization has been tested.
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FailureKindSchema
|
|
3
|
+
} from "./chunk-IHG6ADQG.js";
|
|
4
|
+
|
|
5
|
+
// src/textgen/structured_output.ts
|
|
6
|
+
var keywords = /* @__PURE__ */ new Set(["type", "properties", "required", "additionalProperties", "items", "enum", "const", "description", "title", "minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems", "format"]);
|
|
7
|
+
var movableSchemaKeywords = Object.freeze(["minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems", "format"]);
|
|
8
|
+
var removableSchemaKeywords = Object.freeze([...movableSchemaKeywords, "title", "additionalProperties"]);
|
|
9
|
+
var types = /* @__PURE__ */ new Set(["object", "array", "string", "number", "integer", "boolean", "null"]);
|
|
10
|
+
var formats = /* @__PURE__ */ new Set(["date-time", "date", "time", "uuid"]);
|
|
11
|
+
function jsonPointerError(pointer, reason) {
|
|
12
|
+
return new Error(`at ${pointer || "the root"}: ${reason}`);
|
|
13
|
+
}
|
|
14
|
+
function isJSONObject(value) {
|
|
15
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function pointerSegment(value) {
|
|
18
|
+
return value.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
19
|
+
}
|
|
20
|
+
function boundedNumber(value) {
|
|
21
|
+
return typeof value === "number" && Number.isFinite(value) && parseRational(String(value)) !== void 0;
|
|
22
|
+
}
|
|
23
|
+
function validateCountBounds(node, pointer, minimum, maximum) {
|
|
24
|
+
for (const keyword of [minimum, maximum]) if (keyword in node && (!Number.isSafeInteger(node[keyword]) || node[keyword] < 0)) throw jsonPointerError(pointer, `"${keyword}" must be a non-negative integer`);
|
|
25
|
+
if (minimum in node && maximum in node && node[minimum] > node[maximum]) throw jsonPointerError(pointer, `"${minimum}" exceeds "${maximum}"`);
|
|
26
|
+
}
|
|
27
|
+
function schemaType(node, pointer) {
|
|
28
|
+
const type = node.type;
|
|
29
|
+
if (typeof type === "string" && types.has(type)) return [type, false];
|
|
30
|
+
if (Array.isArray(type) && type.length === 2 && type.every((entry) => typeof entry === "string") && type.includes("null")) {
|
|
31
|
+
const other = type.find((entry) => entry !== "null");
|
|
32
|
+
if (other && types.has(other)) return [other, true];
|
|
33
|
+
}
|
|
34
|
+
throw jsonPointerError(pointer, '"type" must be one JSON type or a pair of one type and "null"');
|
|
35
|
+
}
|
|
36
|
+
function canonicalizePortableSchema(schema) {
|
|
37
|
+
function checkSerializable(value, seen) {
|
|
38
|
+
if (typeof value === "number" && !Number.isFinite(value)) throw new Error("schema must be JSON serializable");
|
|
39
|
+
if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") throw new Error("schema must be JSON serializable");
|
|
40
|
+
if (value && typeof value === "object") {
|
|
41
|
+
if ("toJSON" in value && typeof value.toJSON === "function") return;
|
|
42
|
+
if (seen.has(value)) throw new Error("schema must be JSON serializable");
|
|
43
|
+
seen.add(value);
|
|
44
|
+
for (const child of Object.values(value)) checkSerializable(child, seen);
|
|
45
|
+
seen.delete(value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
checkSerializable(schema, /* @__PURE__ */ new Set());
|
|
49
|
+
let canonical;
|
|
50
|
+
try {
|
|
51
|
+
canonical = JSON.parse(JSON.stringify(schema));
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error("schema must be JSON serializable");
|
|
54
|
+
}
|
|
55
|
+
if (!isJSONObject(canonical)) throw new Error("schema must be a JSON object");
|
|
56
|
+
const [rootType, nullable] = schemaType(canonical, "");
|
|
57
|
+
if (rootType !== "object" || nullable) throw jsonPointerError("", 'the root must have type "object"');
|
|
58
|
+
checkNode(canonical, "", 1);
|
|
59
|
+
return canonical;
|
|
60
|
+
}
|
|
61
|
+
function checkNode(node, pointer, depth) {
|
|
62
|
+
if (depth > 10) throw jsonPointerError(pointer, "the schema is nested deeper than 10 levels");
|
|
63
|
+
for (const keyword of Object.keys(node).sort()) if (!keywords.has(keyword)) throw jsonPointerError(pointer, `keyword ${JSON.stringify(keyword.slice(0, 64))} is outside the portable subset`);
|
|
64
|
+
const [type] = schemaType(node, pointer);
|
|
65
|
+
const keywordRequiredType = { properties: "object", required: "object", additionalProperties: "object", items: "array", minItems: "array", maxItems: "array", minLength: "string", maxLength: "string", format: "string" };
|
|
66
|
+
for (const keyword of Object.keys(node).sort()) {
|
|
67
|
+
if (keywordRequiredType[keyword] && keywordRequiredType[keyword] !== type) throw jsonPointerError(pointer, `keyword "${keyword}" applies only to type "${keywordRequiredType[keyword]}"`);
|
|
68
|
+
if ((keyword === "minimum" || keyword === "maximum") && type !== "number" && type !== "integer") throw jsonPointerError(pointer, `keyword "${keyword}" applies only to number and integer types`);
|
|
69
|
+
}
|
|
70
|
+
for (const keyword of ["description", "title"]) if (keyword in node && typeof node[keyword] !== "string") throw jsonPointerError(pointer, `"${keyword}" must be a string`);
|
|
71
|
+
if ("enum" in node && (!Array.isArray(node.enum) || node.enum.length === 0)) throw jsonPointerError(pointer, '"enum" must be a non-empty array');
|
|
72
|
+
for (const value of node.enum ?? []) if (!scalar(value)) throw jsonPointerError(pointer, '"enum" values must be strings, numbers, booleans, or null');
|
|
73
|
+
if ("const" in node && !scalar(node.const)) throw jsonPointerError(pointer, '"const" must be a string, number, boolean, or null');
|
|
74
|
+
if (type === "object") {
|
|
75
|
+
if (!isJSONObject(node.properties)) throw jsonPointerError(pointer, 'an object requires a "properties" object');
|
|
76
|
+
if (node.additionalProperties !== false) throw jsonPointerError(pointer, 'an object must set "additionalProperties" to false');
|
|
77
|
+
if (!Array.isArray(node.required)) throw jsonPointerError(pointer, 'an object must list every property in "required"');
|
|
78
|
+
const required = node.required;
|
|
79
|
+
if (required.some((name) => typeof name !== "string") || new Set(required).size !== required.length) throw jsonPointerError(pointer, '"required" must list property names once each');
|
|
80
|
+
for (const name of required) if (!Object.hasOwn(node.properties, name)) throw jsonPointerError(pointer, '"required" lists a property that "properties" does not declare');
|
|
81
|
+
if (required.length !== Object.keys(node.properties).length) throw jsonPointerError(pointer, 'an object must list every property in "required"; use type [T, "null"] for an optional value');
|
|
82
|
+
for (const [name, child] of Object.entries(node.properties).sort(([a], [b]) => a.localeCompare(b))) {
|
|
83
|
+
const childPointer = `${pointer}/properties/${pointerSegment(name)}`;
|
|
84
|
+
if (!isJSONObject(child)) throw jsonPointerError(childPointer, "a property schema must be an object");
|
|
85
|
+
checkNode(child, childPointer, depth + 1);
|
|
86
|
+
}
|
|
87
|
+
} else if (type === "array") {
|
|
88
|
+
validateCountBounds(node, pointer, "minItems", "maxItems");
|
|
89
|
+
if (!isJSONObject(node.items)) throw jsonPointerError(pointer, 'an array requires an "items" schema object');
|
|
90
|
+
checkNode(node.items, `${pointer}/items`, depth + 1);
|
|
91
|
+
} else if (type === "string") {
|
|
92
|
+
if ("format" in node && !formats.has(node.format)) throw jsonPointerError(pointer, '"format" must be date-time, date, time, or uuid');
|
|
93
|
+
validateCountBounds(node, pointer, "minLength", "maxLength");
|
|
94
|
+
} else if (type === "number" || type === "integer") {
|
|
95
|
+
for (const keyword of ["minimum", "maximum"]) if (keyword in node && !boundedNumber(node[keyword])) throw jsonPointerError(pointer, `"${keyword}" must be a number`);
|
|
96
|
+
if (node.minimum !== void 0 && node.maximum !== void 0 && node.minimum > node.maximum) throw jsonPointerError(pointer, '"minimum" exceeds "maximum"');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function scalar(value) {
|
|
100
|
+
return value === null || typeof value === "string" || typeof value === "boolean" || boundedNumber(value);
|
|
101
|
+
}
|
|
102
|
+
function transformSchemaForProvider(schema, rules) {
|
|
103
|
+
const copy = JSON.parse(JSON.stringify(schema));
|
|
104
|
+
function visit(node) {
|
|
105
|
+
const constraints = [];
|
|
106
|
+
for (const keyword of [...rules.keywordsMovedToDescription ?? []].sort()) if (Object.hasOwn(node, keyword)) {
|
|
107
|
+
constraints.push(`${keyword}: ${marshalGoJSON(node[keyword])}`);
|
|
108
|
+
delete node[keyword];
|
|
109
|
+
}
|
|
110
|
+
if (constraints.length) node.description = `${typeof node.description === "string" && node.description ? `${node.description} ` : ""}(${constraints.join(", ")})`;
|
|
111
|
+
for (const keyword of rules.keywordsRemoved ?? []) delete node[keyword];
|
|
112
|
+
if (isJSONObject(node.properties)) {
|
|
113
|
+
for (const child of Object.values(node.properties)) if (isJSONObject(child)) visit(child);
|
|
114
|
+
}
|
|
115
|
+
if (isJSONObject(node.items)) visit(node.items);
|
|
116
|
+
}
|
|
117
|
+
visit(copy);
|
|
118
|
+
return copy;
|
|
119
|
+
}
|
|
120
|
+
function jsonObjectInstruction(name, description, schema) {
|
|
121
|
+
return `Respond with only one JSON object, without Markdown code fences, that matches the JSON Schema named ${JSON.stringify(name)}${description ? ` (${description})` : ""}:
|
|
122
|
+
${marshalGoJSON(schema)}`;
|
|
123
|
+
}
|
|
124
|
+
function marshalGoJSON(value) {
|
|
125
|
+
if (Array.isArray(value)) return `[${value.map(marshalGoJSON).join(",")}]`;
|
|
126
|
+
if (isJSONObject(value)) {
|
|
127
|
+
const keys = Object.keys(value).sort((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right)));
|
|
128
|
+
return `{${keys.map((key) => `${marshalGoJSON(key)}:${marshalGoJSON(value[key])}`).join(",")}}`;
|
|
129
|
+
}
|
|
130
|
+
const encoded = JSON.stringify(value);
|
|
131
|
+
if (encoded === void 0) throw new Error("schema must be JSON serializable");
|
|
132
|
+
return encoded.replace(/[<>&\u2028\u2029]/g, (character) => `\\u${character.codePointAt(0).toString(16).padStart(4, "0")}`);
|
|
133
|
+
}
|
|
134
|
+
function extractStructuredAnswer(request, generatedText) {
|
|
135
|
+
if (!request.structuredOutput) return generatedText;
|
|
136
|
+
const schema = canonicalizePortableSchema(request.structuredOutput.schema);
|
|
137
|
+
return extractStructuredAnswerFromSchema(schema, generatedText);
|
|
138
|
+
}
|
|
139
|
+
function extractStructuredAnswerFromSchema(schema, generatedText) {
|
|
140
|
+
let text = generatedText.trim();
|
|
141
|
+
const fence = /^```([^\n]*)\n([\s\S]*)```$/.exec(text);
|
|
142
|
+
if (fence && (!fence[1]?.trim() || fence[1].trim().toLowerCase() === "json")) text = (fence[2] ?? "").trim();
|
|
143
|
+
let value;
|
|
144
|
+
const sourceByHolder = /* @__PURE__ */ new WeakMap();
|
|
145
|
+
try {
|
|
146
|
+
const sourceReviver = function(key, child, context) {
|
|
147
|
+
if (typeof child === "number" && context.source !== void 0) {
|
|
148
|
+
let sources = sourceByHolder.get(this);
|
|
149
|
+
if (!sources) {
|
|
150
|
+
sources = /* @__PURE__ */ new Map();
|
|
151
|
+
sourceByHolder.set(this, sources);
|
|
152
|
+
}
|
|
153
|
+
sources.set(key, context.source);
|
|
154
|
+
}
|
|
155
|
+
return child;
|
|
156
|
+
};
|
|
157
|
+
value = JSON.parse(text, sourceReviver);
|
|
158
|
+
} catch {
|
|
159
|
+
throw jsonPointerError("", "the text is not one JSON value");
|
|
160
|
+
}
|
|
161
|
+
const numbers = /* @__PURE__ */ new Map();
|
|
162
|
+
function assign(current, pointer) {
|
|
163
|
+
if (current === null || typeof current !== "object") return;
|
|
164
|
+
for (const [name, child] of Object.entries(current)) {
|
|
165
|
+
const childPointer = `${pointer}/${pointerSegment(name)}`;
|
|
166
|
+
if (typeof child === "number") numbers.set(childPointer, sourceByHolder.get(current)?.get(name) ?? "");
|
|
167
|
+
else assign(child, childPointer);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
assign(value, "");
|
|
171
|
+
validateInstance(value, schema, "", numbers);
|
|
172
|
+
return text;
|
|
173
|
+
}
|
|
174
|
+
function validateInstance(value, schema, pointer, numbers) {
|
|
175
|
+
const [type, nullable] = schemaType(schema, pointer);
|
|
176
|
+
const number = typeof value === "number" ? parseRational(numbers.get(pointer) ?? "") : void 0;
|
|
177
|
+
if (typeof value === "number" && !number) throw jsonPointerError(pointer, "the number is out of range");
|
|
178
|
+
if (value === null) {
|
|
179
|
+
if (!nullable && type !== "null") throw jsonPointerError(pointer, `expected type ${type}, found null`);
|
|
180
|
+
} else {
|
|
181
|
+
const actualType = Array.isArray(value) ? "array" : isJSONObject(value) ? "object" : typeof value;
|
|
182
|
+
if (!(type === actualType || type === "integer" && number && number.numerator % number.denominator === 0n || type === "number" && number)) throw jsonPointerError(pointer, `expected type ${type}`);
|
|
183
|
+
}
|
|
184
|
+
const equal = (expected) => {
|
|
185
|
+
if (typeof expected === "number" && number) {
|
|
186
|
+
const wanted = parseRational(String(expected));
|
|
187
|
+
return wanted !== void 0 && compareRational(wanted, number) === 0;
|
|
188
|
+
}
|
|
189
|
+
return Object.is(expected, value);
|
|
190
|
+
};
|
|
191
|
+
if (Array.isArray(schema.enum) && !schema.enum.some(equal)) throw jsonPointerError(pointer, "the value is not one of the enum values");
|
|
192
|
+
if ("const" in schema && !equal(schema.const)) throw jsonPointerError(pointer, "the value does not equal const");
|
|
193
|
+
if (value === null) return;
|
|
194
|
+
if (type === "object" && isJSONObject(value) && isJSONObject(schema.properties)) {
|
|
195
|
+
for (const name of Object.keys(value).sort()) if (!Object.hasOwn(schema.properties, name)) throw jsonPointerError(pointer, "the object has a property the schema does not declare");
|
|
196
|
+
for (const [name, child] of Object.entries(schema.properties).sort(([a], [b]) => a.localeCompare(b))) {
|
|
197
|
+
const childPointer = `${pointer}/${pointerSegment(name)}`;
|
|
198
|
+
if (!Object.hasOwn(value, name)) throw jsonPointerError(childPointer, "a required property is missing");
|
|
199
|
+
if (isJSONObject(child)) validateInstance(value[name], child, childPointer, numbers);
|
|
200
|
+
}
|
|
201
|
+
} else if (type === "array" && Array.isArray(value) && isJSONObject(schema.items)) {
|
|
202
|
+
checkInstanceCount(value.length, schema, pointer, "minItems", "maxItems", "items");
|
|
203
|
+
value.forEach((item, index) => validateInstance(item, schema.items, `${pointer}/${index}`, numbers));
|
|
204
|
+
} else if (type === "string" && typeof value === "string") {
|
|
205
|
+
checkInstanceCount([...value].length, schema, pointer, "minLength", "maxLength", "characters");
|
|
206
|
+
if (typeof schema.format === "string" && !matchesFormat(value, schema.format)) throw jsonPointerError(pointer, `the string does not match format ${schema.format}`);
|
|
207
|
+
} else if ((type === "number" || type === "integer") && typeof value === "number") {
|
|
208
|
+
if (number && typeof schema.minimum === "number" && compareRational(number, parseRational(String(schema.minimum))) < 0) throw jsonPointerError(pointer, "the value is below minimum");
|
|
209
|
+
if (number && typeof schema.maximum === "number" && compareRational(number, parseRational(String(schema.maximum))) > 0) throw jsonPointerError(pointer, "the value is above maximum");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function checkInstanceCount(value, schema, pointer, minimum, maximum, unit) {
|
|
213
|
+
if (typeof schema[minimum] === "number" && value < schema[minimum]) throw jsonPointerError(pointer, `the value has fewer ${unit} than ${minimum}`);
|
|
214
|
+
if (typeof schema[maximum] === "number" && value > schema[maximum]) throw jsonPointerError(pointer, `the value has more ${unit} than ${maximum}`);
|
|
215
|
+
}
|
|
216
|
+
function matchesFormat(value, format) {
|
|
217
|
+
if (format === "uuid") return /^[0-9A-Fa-f]{8}-(?:[0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$/.test(value);
|
|
218
|
+
if (format === "date") {
|
|
219
|
+
const match2 = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
220
|
+
return !!match2 && isValidCalendarDate(Number(match2[1]), Number(match2[2]), Number(match2[3]));
|
|
221
|
+
}
|
|
222
|
+
if (format === "time") return matchesGoClock(value);
|
|
223
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(.+)$/.exec(value);
|
|
224
|
+
if (!match || !matchesGoClock(match[4] ?? "")) return false;
|
|
225
|
+
return isValidCalendarDate(Number(match[1]), Number(match[2]), Number(match[3]));
|
|
226
|
+
}
|
|
227
|
+
function isValidCalendarDate(year, month, day) {
|
|
228
|
+
const daysInMonth = [31, year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] ?? 0;
|
|
229
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth;
|
|
230
|
+
}
|
|
231
|
+
function matchesGoClock(value) {
|
|
232
|
+
const match = /^(\d{1,2}):(\d{2}):(\d{2})(?:[.,]\d+)?(?:Z|[+-](\d{2}):(\d{2}))$/.exec(value);
|
|
233
|
+
return !!match && Number(match[1]) < 24 && Number(match[2]) < 60 && Number(match[3]) < 60 && Number(match[4] ?? 0) <= 24 && Number(match[5] ?? 0) <= 60;
|
|
234
|
+
}
|
|
235
|
+
function parseRational(literal) {
|
|
236
|
+
if (!literal || literal.length > 256) return void 0;
|
|
237
|
+
const match = /^(-?)(0|[1-9]\d*)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(literal);
|
|
238
|
+
if (!match) return void 0;
|
|
239
|
+
const exponent = Number(match[4] ?? 0);
|
|
240
|
+
if (!Number.isInteger(exponent) || Math.abs(exponent) > 400) return void 0;
|
|
241
|
+
const digits = (match[2] ?? "") + (match[3] ?? "");
|
|
242
|
+
const power = exponent - (match[3]?.length ?? 0);
|
|
243
|
+
const signed = BigInt(digits) * (match[1] === "-" ? -1n : 1n);
|
|
244
|
+
return power >= 0 ? { numerator: signed * 10n ** BigInt(power), denominator: 1n } : { numerator: signed, denominator: 10n ** BigInt(-power) };
|
|
245
|
+
}
|
|
246
|
+
function compareRational(left, right) {
|
|
247
|
+
const difference = left.numerator * right.denominator - right.numerator * left.denominator;
|
|
248
|
+
return difference < 0n ? -1 : difference > 0n ? 1 : 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/textgen/wire_format.ts
|
|
252
|
+
function temperatureAccepted() {
|
|
253
|
+
return { kind: "accepted" };
|
|
254
|
+
}
|
|
255
|
+
function temperatureRange(minimum, maximum) {
|
|
256
|
+
return { kind: "range", minimum, maximum };
|
|
257
|
+
}
|
|
258
|
+
function temperatureNotAccepted() {
|
|
259
|
+
return { kind: "notAccepted" };
|
|
260
|
+
}
|
|
261
|
+
function validateStructuredOutputRules(rules) {
|
|
262
|
+
if (!["none", "jsonSchema", "jsonObjectWithInstruction"].includes(rules.mode)) throw new TypeError(`structured output mode ${JSON.stringify(rules.mode)} is invalid`);
|
|
263
|
+
for (const keyword of rules.keywordsMovedToDescription ?? []) if (!movableSchemaKeywords.includes(keyword)) throw new TypeError(`schema keyword ${JSON.stringify(keyword)} cannot be moved to a description`);
|
|
264
|
+
for (const keyword of rules.keywordsRemoved ?? []) if (!removableSchemaKeywords.includes(keyword)) throw new TypeError(`schema keyword ${JSON.stringify(keyword)} cannot be removed`);
|
|
265
|
+
}
|
|
266
|
+
function retryOutcome(kind) {
|
|
267
|
+
return { disposition: "retry", kind };
|
|
268
|
+
}
|
|
269
|
+
function providerRejectedOutcome(kind) {
|
|
270
|
+
return { disposition: "providerRejected", kind };
|
|
271
|
+
}
|
|
272
|
+
function quotaExhaustedOutcome() {
|
|
273
|
+
return providerRejectedOutcome("QUOTA_EXHAUSTED");
|
|
274
|
+
}
|
|
275
|
+
function blockedOutcome() {
|
|
276
|
+
return { disposition: "blocked", kind: "PROVIDER_REJECTION" };
|
|
277
|
+
}
|
|
278
|
+
function invalidResponseOutcome() {
|
|
279
|
+
return { disposition: "invalidResponse", kind: "PROTOCOL" };
|
|
280
|
+
}
|
|
281
|
+
var ProviderReportedError = class extends Error {
|
|
282
|
+
/** Bounded machine-readable tokens. */
|
|
283
|
+
errorTokens;
|
|
284
|
+
/** Construct without exposing provider message text. */
|
|
285
|
+
constructor(errorTokens) {
|
|
286
|
+
super("provider reported an error inside a 2xx response");
|
|
287
|
+
this.name = "ProviderReportedError";
|
|
288
|
+
this.errorTokens = errorTokens;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
function classifyError(rules, statusCode, tokens, isReported = false) {
|
|
292
|
+
for (const rule of rules) if ((rule.statusCode === 0 || rule.statusCode === statusCode) && (!isReported || rule.statusCode === 0) && (!rule.errorToken || tokens.includes(rule.errorToken))) return rule.outcome;
|
|
293
|
+
if (isReported) return invalidResponseOutcome();
|
|
294
|
+
if (statusCode === 429) return retryOutcome("RATE_LIMIT");
|
|
295
|
+
if (statusCode === 408 || statusCode >= 500 && statusCode !== 501) return retryOutcome("AVAILABILITY");
|
|
296
|
+
const kinds = { 401: "AUTHENTICATION", 402: "QUOTA_EXHAUSTED", 403: "AUTHORIZATION", 404: "NOT_FOUND", 409: "CONFLICT" };
|
|
297
|
+
if (statusCode >= 300 && statusCode < 400) return invalidResponseOutcome();
|
|
298
|
+
return providerRejectedOutcome(kinds[statusCode] ?? "PROVIDER_REJECTION");
|
|
299
|
+
}
|
|
300
|
+
function validateWireFormat(format) {
|
|
301
|
+
if (!format.providerName) throw new TypeError("wire format provider name is required");
|
|
302
|
+
if (format.modelIDRule !== "body" && format.modelIDRule !== "pathSegment") throw new TypeError("wire format model ID rule is invalid");
|
|
303
|
+
if (typeof format.rulesForModel !== "function" || typeof format.encodeRequest !== "function") throw new TypeError("wire format rulesForModel and encodeRequest are required");
|
|
304
|
+
if (!format.decodeResponse && !format.decodeStreamEvent) throw new TypeError("wire format requires decodeResponse, decodeStreamEvent, or both");
|
|
305
|
+
if (typeof format.credentialHeader?.name !== "string" || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$/.test(format.credentialHeader.name)) throw new TypeError("wire format credential header name is invalid");
|
|
306
|
+
if (typeof format.credentialHeader.prefix !== "string" || !/^[\x20-\x7e]*$/.test(format.credentialHeader.prefix)) throw new TypeError("wire format credential header prefix must be printable ASCII");
|
|
307
|
+
for (const [token, reason] of Object.entries(format.finishReasons)) if (!/^[A-Za-z0-9_.:-]{1,64}$/.test(token) || !["stop", "length", "contentPolicy", "refusal"].includes(reason)) throw new TypeError("wire format finish token is invalid");
|
|
308
|
+
for (const [index, rule] of (format.errorRules ?? []).entries()) {
|
|
309
|
+
if (!Number.isInteger(rule.statusCode) || rule.statusCode !== 0 && (rule.statusCode < 300 || rule.statusCode > 599)) throw new TypeError(`error rule ${index} status is invalid`);
|
|
310
|
+
if (rule.errorToken && !/^[A-Za-z0-9_.:-]{1,64}$/.test(rule.errorToken)) throw new TypeError(`error rule ${index} token is invalid`);
|
|
311
|
+
if (!rule.outcome || !["retry", "providerRejected", "invalidResponse", "blocked"].includes(rule.outcome.disposition) || !FailureKindSchema.safeParse(rule.outcome.kind).success) throw new TypeError(`error rule ${index} outcome is invalid`);
|
|
312
|
+
}
|
|
313
|
+
for (const pointer of format.errorTokenPointers ?? []) if (pointer !== "" && !pointer.startsWith("/")) throw new TypeError("wire format error-token pointer must start with /");
|
|
314
|
+
for (const name of [...format.requestIDHeaders ?? [], ...format.rateLimitHeaders ?? []]) if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$/.test(name)) throw new TypeError("wire format response header name is invalid");
|
|
315
|
+
if (!Number.isSafeInteger(format.stallTimeoutMs ?? 0) || (format.stallTimeoutMs ?? 0) < 0) throw new TypeError("wire format stall timeout cannot be negative");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export {
|
|
319
|
+
canonicalizePortableSchema,
|
|
320
|
+
transformSchemaForProvider,
|
|
321
|
+
jsonObjectInstruction,
|
|
322
|
+
extractStructuredAnswer,
|
|
323
|
+
extractStructuredAnswerFromSchema,
|
|
324
|
+
temperatureAccepted,
|
|
325
|
+
temperatureRange,
|
|
326
|
+
temperatureNotAccepted,
|
|
327
|
+
validateStructuredOutputRules,
|
|
328
|
+
retryOutcome,
|
|
329
|
+
providerRejectedOutcome,
|
|
330
|
+
quotaExhaustedOutcome,
|
|
331
|
+
blockedOutcome,
|
|
332
|
+
invalidResponseOutcome,
|
|
333
|
+
ProviderReportedError,
|
|
334
|
+
classifyError,
|
|
335
|
+
validateWireFormat
|
|
336
|
+
};
|
|
@@ -84,7 +84,7 @@ var AddressRefusal = class extends Error {
|
|
|
84
84
|
reason;
|
|
85
85
|
};
|
|
86
86
|
var ProviderTransportError = class extends Error {
|
|
87
|
-
/** True only when
|
|
87
|
+
/** True only when the request failed before a usable connection could send HTTP bytes. */
|
|
88
88
|
constructor(isProvablyNotSent) {
|
|
89
89
|
super("provider request failed in transport");
|
|
90
90
|
this.isProvablyNotSent = isProvablyNotSent;
|
|
@@ -92,6 +92,13 @@ var ProviderTransportError = class extends Error {
|
|
|
92
92
|
}
|
|
93
93
|
isProvablyNotSent;
|
|
94
94
|
};
|
|
95
|
+
var ProviderStalledError = class extends Error {
|
|
96
|
+
/** Construct a sanitized no-byte timeout failure. */
|
|
97
|
+
constructor() {
|
|
98
|
+
super("provider sent no bytes within the stall timeout");
|
|
99
|
+
this.name = "ProviderStalledError";
|
|
100
|
+
}
|
|
101
|
+
};
|
|
95
102
|
function parseRetryAfter(value, now = /* @__PURE__ */ new Date()) {
|
|
96
103
|
const trimmed = value.trim();
|
|
97
104
|
if (/^[0-9]+$/.test(trimmed)) {
|
|
@@ -153,7 +160,8 @@ function readErrorTokens(body, pointers) {
|
|
|
153
160
|
}
|
|
154
161
|
async function sendProviderRequest(request) {
|
|
155
162
|
if (!Number.isSafeInteger(request.maxResponseBytes) || request.maxResponseBytes < 0) throw new RangeError("response body limit must be a non-negative integer");
|
|
156
|
-
if (request.
|
|
163
|
+
if (request.maxErrorResponseBytes !== void 0 && (!Number.isSafeInteger(request.maxErrorResponseBytes) || request.maxErrorResponseBytes < 0)) throw new RangeError("error body limit must be a non-negative integer");
|
|
164
|
+
if (request.addressPolicy !== "publicHTTPS" && request.addressPolicy !== "privateNetworkHTTP" && request.addressPolicy !== "unrestricted") throw new AddressRefusal("address policy is invalid");
|
|
157
165
|
let parsed;
|
|
158
166
|
try {
|
|
159
167
|
parsed = new URL(request.url);
|
|
@@ -172,7 +180,8 @@ async function sendProviderRequest(request) {
|
|
|
172
180
|
} catch {
|
|
173
181
|
throw new ProviderTransportError(true);
|
|
174
182
|
}
|
|
175
|
-
|
|
183
|
+
const restrictedPolicy = request.addressPolicy === "unrestricted" ? void 0 : request.addressPolicy;
|
|
184
|
+
if (addresses.length === 0 || restrictedPolicy && addresses.some(({ address }) => !isAllowedProviderIPAddress(address, parsed.protocol, restrictedPolicy))) {
|
|
176
185
|
throw new AddressRefusal("resolved address violates policy");
|
|
177
186
|
}
|
|
178
187
|
const selected = addresses[0];
|
|
@@ -181,36 +190,81 @@ async function sendProviderRequest(request) {
|
|
|
181
190
|
method: request.method,
|
|
182
191
|
headers: request.headers,
|
|
183
192
|
signal,
|
|
184
|
-
lookup: (_hostname,
|
|
193
|
+
lookup: (_hostname, options2, callback) => options2?.all ? callback(null, [{ address: selected.address, family: selected.family }]) : callback(null, selected.address, selected.family)
|
|
185
194
|
};
|
|
186
195
|
return new Promise((resolve, reject) => {
|
|
187
|
-
let
|
|
196
|
+
let hasConnected = false;
|
|
188
197
|
let hasWrittenRequest = false;
|
|
189
198
|
let clientRequest;
|
|
199
|
+
let responseStallTimer;
|
|
190
200
|
try {
|
|
191
201
|
clientRequest = (parsed.protocol === "https:" ? https : http).request(parsed, options, (response) => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
if (responseStallTimer) clearTimeout(responseStallTimer);
|
|
203
|
+
const chunks = [];
|
|
204
|
+
let size = 0;
|
|
205
|
+
let stallTimer;
|
|
206
|
+
const observeProgress = () => {
|
|
207
|
+
if (stallTimer) clearTimeout(stallTimer);
|
|
208
|
+
if (request.stallTimeoutMs && request.stallTimeoutMs > 0) stallTimer = setTimeout(() => response.destroy(new ProviderStalledError()), request.stallTimeoutMs);
|
|
209
|
+
};
|
|
210
|
+
observeProgress();
|
|
211
|
+
void (async () => {
|
|
212
|
+
try {
|
|
213
|
+
for await (const chunk of response) {
|
|
214
|
+
observeProgress();
|
|
215
|
+
const isSuccess = (response.statusCode ?? 0) >= 200 && (response.statusCode ?? 0) <= 299;
|
|
216
|
+
const limit = isSuccess ? request.maxResponseBytes : request.maxErrorResponseBytes ?? request.maxResponseBytes;
|
|
217
|
+
if (!isSuccess && request.maxErrorResponseBytes !== void 0 && size + chunk.length > limit) {
|
|
218
|
+
chunks.push(chunk.subarray(0, Math.max(0, limit - size)));
|
|
219
|
+
response.destroy();
|
|
220
|
+
resolve({ status: response.statusCode ?? 0, headers: response.headers, body: Buffer.concat(chunks) });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const shouldContinue = request.onResponseChunk?.(chunk, response.statusCode ?? 0, response.headers);
|
|
224
|
+
if (shouldContinue === false) {
|
|
225
|
+
response.destroy();
|
|
226
|
+
resolve({ status: response.statusCode ?? 0, headers: response.headers, body: Buffer.concat(chunks) });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
size += chunk.length;
|
|
230
|
+
if (size > limit) throw new BodyTooLargeError(response.statusCode ?? 0, response.headers);
|
|
231
|
+
chunks.push(chunk);
|
|
232
|
+
}
|
|
233
|
+
resolve({ status: response.statusCode ?? 0, headers: response.headers, body: Buffer.concat(chunks) });
|
|
234
|
+
} catch (error) {
|
|
235
|
+
response.destroy();
|
|
236
|
+
if (request.ignoreErrorBodyReadFailure && ((response.statusCode ?? 0) < 200 || (response.statusCode ?? 0) >= 300)) {
|
|
237
|
+
resolve({ status: response.statusCode ?? 0, headers: response.headers, body: Buffer.concat(chunks) });
|
|
238
|
+
} else {
|
|
239
|
+
reject(error instanceof BodyTooLargeError || error instanceof ProviderStalledError ? error : new ProviderTransportError(false));
|
|
240
|
+
}
|
|
241
|
+
} finally {
|
|
242
|
+
if (stallTimer) clearTimeout(stallTimer);
|
|
243
|
+
}
|
|
244
|
+
})();
|
|
198
245
|
});
|
|
199
246
|
} catch {
|
|
200
247
|
reject(new ProviderTransportError(true));
|
|
201
248
|
return;
|
|
202
249
|
}
|
|
203
|
-
clientRequest.on("socket", () => {
|
|
204
|
-
|
|
250
|
+
clientRequest.on("socket", (socket) => {
|
|
251
|
+
if (!socket.connecting) hasConnected = true;
|
|
252
|
+
else socket.once(parsed.protocol === "https:" ? "secureConnect" : "connect", () => {
|
|
253
|
+
hasConnected = true;
|
|
254
|
+
});
|
|
205
255
|
});
|
|
206
256
|
clientRequest.on("finish", () => {
|
|
207
257
|
hasWrittenRequest = true;
|
|
208
258
|
});
|
|
209
|
-
clientRequest.on("error", () =>
|
|
259
|
+
clientRequest.on("error", (error) => {
|
|
260
|
+
if (responseStallTimer) clearTimeout(responseStallTimer);
|
|
261
|
+
reject(error instanceof ProviderStalledError ? error : new ProviderTransportError(!hasConnected && !hasWrittenRequest));
|
|
262
|
+
});
|
|
263
|
+
if (request.stallTimeoutMs && request.stallTimeoutMs > 0) responseStallTimer = setTimeout(() => clientRequest.destroy(new ProviderStalledError()), request.stallTimeoutMs);
|
|
210
264
|
try {
|
|
211
265
|
clientRequest.end(request.body);
|
|
212
266
|
} catch {
|
|
213
|
-
reject(new ProviderTransportError(!
|
|
267
|
+
reject(new ProviderTransportError(!hasConnected && !hasWrittenRequest));
|
|
214
268
|
}
|
|
215
269
|
});
|
|
216
270
|
}
|
|
@@ -239,6 +293,7 @@ export {
|
|
|
239
293
|
BodyTooLargeError,
|
|
240
294
|
AddressRefusal,
|
|
241
295
|
ProviderTransportError,
|
|
296
|
+
ProviderStalledError,
|
|
242
297
|
parseRetryAfter,
|
|
243
298
|
validateBaseURL,
|
|
244
299
|
isHeaderSafeCredential,
|