@superdurable/connectors-sdk 0.2.0 → 0.3.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 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.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`.
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.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
 
@@ -119,6 +119,7 @@ if (result.kind !== 'branch' || retryPolicy.totalDurationMs !== 3600000 || crede
119
119
 
120
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.
121
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.
122
+ `ProviderRequest.onBodyLimit` defaults to `destroy`, closing an oversized response immediately. Set it to `drain` for provider calls that continue or bill work after a client disconnect: the SDK releases buffered bytes, delivers callback bytes only through the limit, reads to EOF, then rejects with `BodyTooLargeError`. Drain requires at least one positive `timeoutMs`, positive `stallTimeoutMs`, or `signal`; otherwise `sendProviderRequest` throws `TypeError`. These bounds remain active while draining. For non-2xx responses, an explicit `maxErrorResponseBytes` takes precedence and truncates the body immediately instead of draining. Once drain begins, a later read failure still rejects with `BodyTooLargeError`, including when `ignoreErrorBodyReadFailure` is set. The runnable example exercises `drain` with a local server.
122
123
 
123
124
  ```js
124
125
  import { createServer } from 'node:http';
@@ -230,7 +231,7 @@ mutation.assertNoFailures();
230
231
 
231
232
  ## OAuth token refresh
232
233
 
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
+ 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 use `authMethodId`; an empty value selects `defaultMethod`. Refresh validation uses `requiredScopes` when declared, and a grant can override `exactScopes` or `acceptsMissingTokenType`. `refresh.scopeSeparators` selects `whitespace` (the default) or `whitespaceAndComma` for both exact and all-scope checks. A blank client secret requires reauthorization unless `refresh.allowsPublicClient` is true. A response lacking `expires_in` is retryable unless the manifest declares `missingExpiry`. Only declared terminal OAuth errors below HTTP 500 require reauthorization; transport, server, and other token errors remain retryable.
234
235
 
235
236
  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.
236
237
 
@@ -280,6 +281,12 @@ await fence.recordProviderId(result.providerId);
280
281
 
281
282
  ## Release notes
282
283
 
284
+ ### 0.3.0
285
+
286
+ Adds declaration-driven required and exact scope checks, per-grant token-type rules, Go-compatible duration validation, and default-method selection for manifest refresh drivers. Scope parsing now follows `scopeSeparators`: whitespace by default, or spaces and commas when declared. A blank client secret now requires reauthorization unless public-client refresh is declared. A missing `expires_in` is retryable unless the manifest declares `missingExpiry`. These changes alter the published 0.2.0 refresh driver's behavior.
287
+
288
+ Public HTTPS address checks now refuse the IPv4 broadcast address, matching Go's global-unicast rule. Adds opt-in draining of oversized Provider HTTP responses with bounded cancellation and byte-exact chunk callbacks.
289
+
283
290
  ### 0.2.0
284
291
 
285
292
  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.
@@ -43,7 +43,7 @@ function isLinkLocalIPAddressLiteral(hostname) {
43
43
  }
44
44
  function isAlwaysRefusedIPAddressLiteral(hostname) {
45
45
  const address = unbracketHostname(hostname);
46
- return address === "0.0.0.0" || address === "::" || address.toLowerCase() === "fd00:ec2::254" || contains(linkLocal, address) || contains(multicast, address);
46
+ return address === "0.0.0.0" || address === "255.255.255.255" || address === "::" || address.toLowerCase() === "fd00:ec2::254" || contains(linkLocal, address) || contains(multicast, address);
47
47
  }
48
48
  function isAllowedProviderIPAddress(address, protocol, policy) {
49
49
  if (isIP(unbracketHostname(address)) === 0 || isAlwaysRefusedIPAddressLiteral(address)) return false;
@@ -161,6 +161,8 @@ function readErrorTokens(body, pointers) {
161
161
  async function sendProviderRequest(request) {
162
162
  if (!Number.isSafeInteger(request.maxResponseBytes) || request.maxResponseBytes < 0) throw new RangeError("response body limit must be a non-negative integer");
163
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.onBodyLimit !== void 0 && request.onBodyLimit !== "destroy" && request.onBodyLimit !== "drain") throw new TypeError("response body limit action is invalid");
165
+ if (request.onBodyLimit === "drain" && !(request.timeoutMs && request.timeoutMs > 0) && !(request.stallTimeoutMs && request.stallTimeoutMs > 0) && !request.signal) throw new TypeError("draining requires a timeout or abort signal");
164
166
  if (request.addressPolicy !== "publicHTTPS" && request.addressPolicy !== "privateNetworkHTTP" && request.addressPolicy !== "unrestricted") throw new AddressRefusal("address policy is invalid");
165
167
  let parsed;
166
168
  try {
@@ -195,13 +197,16 @@ async function sendProviderRequest(request) {
195
197
  return new Promise((resolve, reject) => {
196
198
  let hasConnected = false;
197
199
  let hasWrittenRequest = false;
200
+ let hasReceivedResponse = false;
198
201
  let clientRequest;
199
202
  let responseStallTimer;
200
203
  try {
201
204
  clientRequest = (parsed.protocol === "https:" ? https : http).request(parsed, options, (response) => {
205
+ hasReceivedResponse = true;
202
206
  if (responseStallTimer) clearTimeout(responseStallTimer);
203
207
  const chunks = [];
204
208
  let size = 0;
209
+ let isBodyTooLarge = false;
205
210
  let stallTimer;
206
211
  const observeProgress = () => {
207
212
  if (stallTimer) clearTimeout(stallTimer);
@@ -212,6 +217,7 @@ async function sendProviderRequest(request) {
212
217
  try {
213
218
  for await (const chunk of response) {
214
219
  observeProgress();
220
+ if (isBodyTooLarge) continue;
215
221
  const isSuccess = (response.statusCode ?? 0) >= 200 && (response.statusCode ?? 0) <= 299;
216
222
  const limit = isSuccess ? request.maxResponseBytes : request.maxErrorResponseBytes ?? request.maxResponseBytes;
217
223
  if (!isSuccess && request.maxErrorResponseBytes !== void 0 && size + chunk.length > limit) {
@@ -220,6 +226,13 @@ async function sendProviderRequest(request) {
220
226
  resolve({ status: response.statusCode ?? 0, headers: response.headers, body: Buffer.concat(chunks) });
221
227
  return;
222
228
  }
229
+ if (request.onBodyLimit === "drain" && size + chunk.length > limit) {
230
+ const remainingBytes = limit - size;
231
+ isBodyTooLarge = true;
232
+ chunks.length = 0;
233
+ if (remainingBytes > 0) request.onResponseChunk?.(chunk.subarray(0, remainingBytes), response.statusCode ?? 0, response.headers);
234
+ continue;
235
+ }
223
236
  const shouldContinue = request.onResponseChunk?.(chunk, response.statusCode ?? 0, response.headers);
224
237
  if (shouldContinue === false) {
225
238
  response.destroy();
@@ -230,10 +243,16 @@ async function sendProviderRequest(request) {
230
243
  if (size > limit) throw new BodyTooLargeError(response.statusCode ?? 0, response.headers);
231
244
  chunks.push(chunk);
232
245
  }
246
+ if (isBodyTooLarge) {
247
+ reject(new BodyTooLargeError(response.statusCode ?? 0, response.headers));
248
+ return;
249
+ }
233
250
  resolve({ status: response.statusCode ?? 0, headers: response.headers, body: Buffer.concat(chunks) });
234
251
  } catch (error) {
235
252
  response.destroy();
236
- if (request.ignoreErrorBodyReadFailure && ((response.statusCode ?? 0) < 200 || (response.statusCode ?? 0) >= 300)) {
253
+ if (isBodyTooLarge) {
254
+ reject(new BodyTooLargeError(response.statusCode ?? 0, response.headers));
255
+ } else if (request.ignoreErrorBodyReadFailure && ((response.statusCode ?? 0) < 200 || (response.statusCode ?? 0) >= 300)) {
237
256
  resolve({ status: response.statusCode ?? 0, headers: response.headers, body: Buffer.concat(chunks) });
238
257
  } else {
239
258
  reject(error instanceof BodyTooLargeError || error instanceof ProviderStalledError ? error : new ProviderTransportError(false));
@@ -258,7 +277,7 @@ async function sendProviderRequest(request) {
258
277
  });
259
278
  clientRequest.on("error", (error) => {
260
279
  if (responseStallTimer) clearTimeout(responseStallTimer);
261
- reject(error instanceof ProviderStalledError ? error : new ProviderTransportError(!hasConnected && !hasWrittenRequest));
280
+ if (!hasReceivedResponse) reject(error instanceof ProviderStalledError ? error : new ProviderTransportError(!hasConnected && !hasWrittenRequest));
262
281
  });
263
282
  if (request.stallTimeoutMs && request.stallTimeoutMs > 0) responseStallTimer = setTimeout(() => clientRequest.destroy(new ProviderStalledError()), request.stallTimeoutMs);
264
283
  try {
@@ -19,7 +19,7 @@ interface ManifestCredentialRefreshOptions {
19
19
  * Missing expiry keeps an existing access token. Refresh sends each active grant
20
20
  * and reports the earliest expiry. Declared sub-500 terminal errors require
21
21
  * reauthorization; other token endpoint failures remain retryable. A credential
22
- * with multiple methods must carry authMethodId.
22
+ * with multiple methods uses the declared default when authMethodId is empty.
23
23
  */
24
24
  declare class ManifestCredentialRefreshDriver<C extends object> implements CredentialRefreshDriver<C> {
25
25
  #private;
@@ -47,7 +47,7 @@ var ManifestCredentialRefreshDriver = class {
47
47
  if (grant.refreshField && !this.#credentialValue(credentials, grant.refreshField)) continue;
48
48
  const token = await this.#exchange(grant, credentials, method, signal);
49
49
  const requiredScopes = this.#scopes(grant.scopes).filter((scope) => grant.kind !== "refresh" || scope !== "offline_access");
50
- if (grants.length > 1 ? !hasExactScopes(token.scope, requiredScopes) : token.scope !== "" && !hasAllScopes(token.scope, requiredScopes)) throw new ReauthorizationRequiredError();
50
+ if (grant.exactScopes ? !hasDeclaredScopes(token.scope, requiredScopes, grant.scopeSeparators, true) : token.scope.trim() !== "" && !hasDeclaredScopes(token.scope, requiredScopes, grant.scopeSeparators, false)) throw new ReauthorizationRequiredError();
51
51
  const updates = {};
52
52
  for (const mapping of grant.mappings) {
53
53
  const destination = manifestText(mapping.credential);
@@ -75,7 +75,6 @@ var ManifestCredentialRefreshDriver = class {
75
75
  const methods = manifestList(this.#auth.methods);
76
76
  if (!methods.length) return this.#auth;
77
77
  const selected = manifestText(credentials.authMethodId);
78
- if (!selected && methods.length > 1) throw new ReauthorizationRequiredError();
79
78
  const method = methods.map(manifestObject).find((item) => item.id === (selected || manifestText(this.#auth.defaultMethod)));
80
79
  if (!method) throw new ReauthorizationRequiredError();
81
80
  return method;
@@ -102,11 +101,15 @@ var ManifestCredentialRefreshDriver = class {
102
101
  assertion: {},
103
102
  mappings: manifestList(declaration.credentialMappings).map(manifestObject),
104
103
  copies: [],
105
- scopes: declaration.grantedScopes === "userScopes" ? oauth.userScopes : oauth.scopes,
104
+ scopes: declaration.grantedScopes === "userScopes" ? oauth.userScopes : refresh?.requiredScopes ?? oauth.scopes,
106
105
  requestScopes: [],
107
106
  terminalCodes: strings(refresh?.terminalErrorCodes, ["invalid_grant"]),
108
107
  acceptedTokenTypes: strings(declaration.acceptedTokenTypes ?? refresh?.acceptedTokenTypes, ["Bearer"]),
109
- acceptsMissingTokenType: refresh?.acceptsMissingTokenType === true,
108
+ exactScopes: declaration.exactScopes === true || declaration.exactScopes === void 0 && refresh?.exactScopes === true,
109
+ scopeSeparators: refresh?.scopeSeparators === "whitespaceAndComma" ? "whitespaceAndComma" : "whitespace",
110
+ allowsPublicClient: refresh?.allowsPublicClient === true,
111
+ acceptsMissingTokenType: declaration.acceptsMissingTokenType === void 0 ? refresh?.acceptsMissingTokenType === true : declaration.acceptsMissingTokenType === true,
112
+ acceptsMissingExpiresIn: refresh?.missingExpiry !== void 0,
110
113
  missingExpiry: missingExpiryLifetime(refresh?.missingExpiry),
111
114
  additionalParameters: stringRecord(refresh?.additionalParameters)
112
115
  }));
@@ -129,7 +132,11 @@ var ManifestCredentialRefreshDriver = class {
129
132
  requestScopes: client.scopes,
130
133
  terminalCodes: strings(client.terminalErrorCodes, ["invalid_client"]),
131
134
  acceptedTokenTypes: strings(client.acceptedTokenTypes, ["Bearer"]),
135
+ exactScopes: false,
136
+ scopeSeparators: "whitespace",
137
+ allowsPublicClient: false,
132
138
  acceptsMissingTokenType: client.acceptsMissingTokenType === true,
139
+ acceptsMissingExpiresIn: client.missingExpiry !== void 0,
133
140
  missingExpiry: missingExpiryLifetime(client.missingExpiry),
134
141
  additionalParameters: stringRecord(client.additionalParameters)
135
142
  }];
@@ -153,7 +160,11 @@ var ManifestCredentialRefreshDriver = class {
153
160
  requestScopes: [],
154
161
  terminalCodes: strings(bearer.terminalErrorCodes, ["invalid_grant"]),
155
162
  acceptedTokenTypes: strings(bearer.acceptedTokenTypes, ["Bearer"]),
163
+ exactScopes: false,
164
+ scopeSeparators: "whitespace",
165
+ allowsPublicClient: false,
156
166
  acceptsMissingTokenType: bearer.acceptsMissingTokenType === true,
167
+ acceptsMissingExpiresIn: bearer.missingExpiry !== void 0,
157
168
  missingExpiry: missingExpiryLifetime(bearer.missingExpiry),
158
169
  additionalParameters: {}
159
170
  }];
@@ -173,7 +184,7 @@ var ManifestCredentialRefreshDriver = class {
173
184
  terminalErrorCodes: grant.terminalCodes,
174
185
  acceptedTokenTypes: grant.acceptedTokenTypes,
175
186
  acceptsMissingTokenType: grant.acceptsMissingTokenType,
176
- acceptsMissingExpiresIn: true,
187
+ acceptsMissingExpiresIn: grant.acceptsMissingExpiresIn,
177
188
  usesJSONRequestBody: grant.encoding === "json",
178
189
  additionalRequestParameters: grant.additionalParameters,
179
190
  credentialMappingPaths: grant.mappings.map((mapping) => manifestText(mapping.source)),
@@ -198,7 +209,8 @@ var ManifestCredentialRefreshDriver = class {
198
209
  return await endpoint.exchangeJWTBearerAssertion(signed, signal);
199
210
  }
200
211
  const secret = this.#credentialValue(credentials, grant.clientSecret);
201
- if (!secret && this.#isRequired(method, grant.clientSecret)) throw new ReauthorizationRequiredError();
212
+ if (!secret && !grant.allowsPublicClient && (grant.kind === "refresh" || this.#isRequired(method, grant.clientSecret)))
213
+ throw new ReauthorizationRequiredError();
202
214
  const authenticationMethod = secret || grant.kind === "client_credentials" ? this.#resolveSource(grant.authentication, credentials, key) : "none";
203
215
  const client = { id: this.#credentialValue(credentials, grant.clientID), secret: new SecretString(secret), authenticationMethod };
204
216
  if (grant.kind === "refresh") {
@@ -228,8 +240,10 @@ var ManifestCredentialRefreshDriver = class {
228
240
  if (!this.#credentialFields[manifestText(copy.credential)] || !this.#credentialFields[manifestText(copy.fromCredential)])
229
241
  throw new TypeError("manifest credential field is not mapped");
230
242
  }
231
- parseDurationMs(grant.missingExpiry);
232
- if (grant.kind === "jwt_bearer") parseDurationMs(manifestText(grant.assertion.lifetime));
243
+ if (grant.missingExpiry && parseDurationMs(grant.missingExpiry) <= 0)
244
+ throw new TypeError("manifest grant duration must be positive");
245
+ if (grant.kind === "jwt_bearer" && parseDurationMs(manifestText(grant.assertion.lifetime)) < 1)
246
+ throw new TypeError("manifest JWT assertion lifetime must be at least one millisecond");
233
247
  }
234
248
  #scopes(source) {
235
249
  if (Array.isArray(source)) return source.filter((value) => typeof value === "string");
@@ -274,37 +288,43 @@ function stringRecord(value) {
274
288
  function missingExpiryLifetime(value) {
275
289
  return manifestText(manifestObject(value).lifetime);
276
290
  }
291
+ function hasDeclaredScopes(returnedScopes, requiredScopes, separators, exact) {
292
+ const granted = new Set(returnedScopes.split(separators === "whitespaceAndComma" ? /[ ,]+/ : /\s+/).filter(Boolean));
293
+ const required = new Set(requiredScopes);
294
+ return (!exact || granted.size === required.size) && [...required].every((scope) => granted.has(scope));
295
+ }
277
296
  function pathEscape(value) {
278
297
  return encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`).replace(/%(?:2B|26|3D|3A|40|24)/g, (escaped) => ({ "%2B": "+", "%26": "&", "%3D": "=", "%3A": ":", "%40": "@", "%24": "$" })[escaped] ?? escaped);
279
298
  }
280
- function hasExactScopes(returnedScopes, requiredScopes) {
281
- const granted = new Set(returnedScopes.split(/[\s,]+/).filter(Boolean));
282
- const required = new Set(requiredScopes);
283
- return granted.size === required.size && [...required].every((scope) => granted.has(scope));
284
- }
285
299
  function parseDurationMs(value) {
286
300
  if (!value) return 0;
287
- const parts = value.matchAll(/(\d+(?:\.\d+)?)(ns|us|µs|μs|ms|s|m|h)/g);
288
- const milliseconds = {
289
- ns: 1e-6,
290
- us: 1e-3,
291
- "\xB5s": 1e-3,
292
- "\u03BCs": 1e-3,
293
- ms: 1,
294
- s: 1e3,
295
- m: 6e4,
296
- h: 36e5
301
+ if (value === "0" || value === "+0" || value === "-0") return 0;
302
+ const sign2 = value[0] === "-" ? -1n : 1n;
303
+ const magnitude = value[0] === "+" || value[0] === "-" ? value.slice(1) : value;
304
+ const parts = magnitude.matchAll(/((?:\d+(?:\.\d*)?|\.\d+))(ns|us|µs|μs|ms|s|m|h)/g);
305
+ const nanoseconds = {
306
+ ns: 1n,
307
+ us: 1000n,
308
+ "\xB5s": 1000n,
309
+ "\u03BCs": 1000n,
310
+ ms: 1000000n,
311
+ s: 1000000000n,
312
+ m: 60000000000n,
313
+ h: 3600000000000n
297
314
  };
298
- let duration = 0;
315
+ let duration = 0n;
299
316
  let position = 0;
300
317
  for (const part of parts) {
301
318
  if (part.index !== position) throw new TypeError("manifest grant duration is invalid");
302
- duration += Number(part[1]) * milliseconds[part[2]];
319
+ const [whole, fraction = ""] = part[1].split(".");
320
+ const unit = nanoseconds[part[2]];
321
+ duration += BigInt(whole || "0") * unit;
322
+ if (fraction) duration += BigInt(fraction) * unit / 10n ** BigInt(fraction.length);
303
323
  position += part[0].length;
304
324
  }
305
- if (position !== value.length || duration <= 0 || !Number.isFinite(duration) || duration > Number.MAX_SAFE_INTEGER)
325
+ if (position !== magnitude.length || duration === 0n || duration > (sign2 < 0n ? 9223372036854775808n : 9223372036854775807n))
306
326
  throw new TypeError("manifest grant duration is invalid");
307
- return duration;
327
+ return Number(sign2 * duration) / 1e6;
308
328
  }
309
329
 
310
330
  // src/oauthtoken/index.ts
@@ -514,7 +534,7 @@ function hasAllScopes(returnedScope, requiredScopes) {
514
534
  const granted = new Set(returnedScope.split(/[ ,]+/).filter(Boolean));
515
535
  return requiredScopes.every((scope) => granted.has(scope));
516
536
  }
517
- function hasExactScopes2(returnedScope, requiredScopes) {
537
+ function hasExactScopes(returnedScope, requiredScopes) {
518
538
  const granted = new Set(returnedScope.split(/[ ,]+/).filter(Boolean));
519
539
  return granted.size === new Set(requiredScopes).size && hasAllScopes(returnedScope, requiredScopes);
520
540
  }
@@ -548,7 +568,7 @@ export {
548
568
  TokenResponse,
549
569
  forcedRefreshFloorMs,
550
570
  hasAllScopes,
551
- hasExactScopes2 as hasExactScopes,
571
+ hasExactScopes,
552
572
  isRefreshRequired,
553
573
  isRefreshRequiredWithIssueTime,
554
574
  parseRSAPrivateKeyPEM,
@@ -6,7 +6,7 @@ import {
6
6
  isLocalStorageHost,
7
7
  isPrivateBrokerHost,
8
8
  readBoundedBody
9
- } from "../chunk-BG65RPGH.js";
9
+ } from "../chunk-UWB627CI.js";
10
10
  import {
11
11
  ReauthorizationRequiredError
12
12
  } from "../chunk-IHG6ADQG.js";
@@ -44,7 +44,8 @@ interface ProviderRequest {
44
44
  /** Request headers, including provider credentials when needed. */ headers?: Record<string, string>;
45
45
  /** Caller-encoded request body. */ body?: Uint8Array;
46
46
  /** Maximum response body size in bytes. */ maxResponseBytes: number;
47
- /** Optional separate prefix limit for non-2xx error bodies. */ maxErrorResponseBytes?: number;
47
+ /** On a response limit, destroy the connection (default) or drain to EOF before rejecting. Drain requires a positive timeoutMs, positive stallTimeoutMs, or signal. */ onBodyLimit?: 'destroy' | 'drain';
48
+ /** Optional separate prefix limit for non-2xx error bodies; when set, its truncation takes precedence over drain. */ maxErrorResponseBytes?: number;
48
49
  /** Classify a non-2xx status even when its diagnostic body fails to read. */ ignoreErrorBodyReadFailure?: boolean;
49
50
  /** Network policy checked against all resolved addresses. */ addressPolicy: AddressPolicy;
50
51
  /** Request deadline in milliseconds. */ timeoutMs?: number;
@@ -11,7 +11,7 @@ import {
11
11
  readErrorTokens,
12
12
  sendProviderRequest,
13
13
  validateBaseURL
14
- } from "../chunk-BG65RPGH.js";
14
+ } from "../chunk-UWB627CI.js";
15
15
  export {
16
16
  AddressRefusal,
17
17
  BodyTooLargeError,
@@ -37,7 +37,7 @@ import {
37
37
  readErrorTokens,
38
38
  sendProviderRequest,
39
39
  validateBaseURL
40
- } from "../chunk-BG65RPGH.js";
40
+ } from "../chunk-UWB627CI.js";
41
41
  import "../chunk-IHG6ADQG.js";
42
42
 
43
43
  // src/textgen/contract.ts
@@ -5,7 +5,7 @@ import {
5
5
  } from "../../chunk-466BPS3M.js";
6
6
  import {
7
7
  readErrorTokens
8
- } from "../../chunk-BG65RPGH.js";
8
+ } from "../../chunk-UWB627CI.js";
9
9
  import "../../chunk-IHG6ADQG.js";
10
10
 
11
11
  // src/textgen/openaichat/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superdurable/connectors-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Provider-neutral TypeScript contracts for Dex connectors",
5
5
  "repository": {
6
6
  "type": "git",