@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 +9 -2
- package/dist/{chunk-BG65RPGH.js → chunk-UWB627CI.js} +22 -3
- package/dist/oauthtoken/index.d.ts +1 -1
- package/dist/oauthtoken/index.js +49 -29
- package/dist/projectconfig/index.js +1 -1
- package/dist/providerhttp/index.d.ts +2 -1
- package/dist/providerhttp/index.js +1 -1
- package/dist/textgen/index.js +1 -1
- package/dist/textgen/openaichat/index.js +1 -1
- package/package.json +1 -1
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.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
|
|
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 (
|
|
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
|
|
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;
|
package/dist/oauthtoken/index.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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:
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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 =
|
|
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
|
-
|
|
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 !==
|
|
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
|
|
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
|
-
|
|
571
|
+
hasExactScopes,
|
|
552
572
|
isRefreshRequired,
|
|
553
573
|
isRefreshRequiredWithIssueTime,
|
|
554
574
|
parseRSAPrivateKeyPEM,
|
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/textgen/index.js
CHANGED