Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"types": "./dist/cjs/index.d.ts",
"exports": {
".": {
"types": "./dist/cjs/index.d.ts",
"import": {
"types": "./dist/esm/index.d.mts",
"default": "./dist/esm/index.mjs"
Expand Down
9 changes: 0 additions & 9 deletions src/management/api/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3421,14 +3421,6 @@ export type ConnectionDomainGoogleApps = string;
*/
export type ConnectionDomainOkta = string;

/** Algorithm used for DPoP proof JWT signing. */
export const ConnectionDpopSigningAlgEnum = {
Es256: "ES256",
Ed25519: "Ed25519",
} as const;
export type ConnectionDpopSigningAlgEnum =
(typeof ConnectionDpopSigningAlgEnum)[keyof typeof ConnectionDpopSigningAlgEnum];

/**
* JSON array containing a list of the JWS signing algorithms (alg values) supported for DPoP proof JWT signing.
*/
Expand Down Expand Up @@ -4372,7 +4364,6 @@ export interface ConnectionOptionsCommonOidc {
client_secret?: Management.ConnectionClientSecretOidc | undefined;
connection_settings?: Management.ConnectionConnectionSettings | undefined;
domain_aliases?: Management.ConnectionDomainAliases | undefined;
dpop_signing_alg?: Management.ConnectionDpopSigningAlgEnum | undefined;
federated_connections_access_tokens?: (Management.ConnectionFederatedConnectionsAccessTokens | null) | undefined;
icon_url?: Management.ConnectionIconUrl | undefined;
id_token_signed_response_algs?: ((Management.ConnectionIdTokenSignedResponseAlgs | undefined) | null) | undefined;
Expand Down
1 change: 1 addition & 0 deletions src/management/core/fetcher/Fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ export async function fetcherImpl<R = unknown>(args: Fetcher.Args): Promise<APIR
args.abortSignal,
args.withCredentials,
args.duplex,
args.responseType === "streaming" || args.responseType === "sse",
),
args.maxRetries,
);
Expand Down
28 changes: 28 additions & 0 deletions src/management/core/fetcher/makeRequest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
import { anySignal, getTimeoutSignal } from "./signals.js";

/**
* Cached result of checking whether the current runtime supports
* the `cache` option in `Request`. Some runtimes (e.g. Cloudflare Workers)
* throw a TypeError when this option is used.
*/
let _cacheNoStoreSupported: boolean | undefined;
export function isCacheNoStoreSupported(): boolean {
if (_cacheNoStoreSupported != null) {
return _cacheNoStoreSupported;
}
try {
new Request("http://localhost", { cache: "no-store" });
_cacheNoStoreSupported = true;
} catch {
_cacheNoStoreSupported = false;
}
return _cacheNoStoreSupported;
}

/**
* Reset the cached result of `isCacheNoStoreSupported`. Exposed for testing only.
*/
export function resetCacheNoStoreSupported(): void {
_cacheNoStoreSupported = undefined;
}

export const makeRequest = async (
fetchFn: (url: string, init: RequestInit) => Promise<Response>,
url: string,
Expand All @@ -10,6 +36,7 @@ export const makeRequest = async (
abortSignal?: AbortSignal,
withCredentials?: boolean,
duplex?: "half",
disableCache?: boolean,
): Promise<Response> => {
const signals: AbortSignal[] = [];

Expand All @@ -32,6 +59,7 @@ export const makeRequest = async (
credentials: withCredentials ? "include" : undefined,
// @ts-ignore
duplex,
...(disableCache && isCacheNoStoreSupported() ? { cache: "no-store" as RequestCache } : {}),
});

if (timeoutAbortId != null) {
Expand Down
16 changes: 8 additions & 8 deletions src/management/core/runtime/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,18 @@ function evaluateRuntime(): Runtime {

/**
* A constant that indicates whether the environment the code is running is Node.JS.
*
* We assign `process` to a local variable first to avoid being flagged by
* bundlers that perform static analysis on `process.versions` (e.g. Next.js
* Edge Runtime warns about Node.js APIs even when they are guarded).
*/
const isNode =
typeof process !== "undefined" &&
"version" in process &&
!!process.version &&
"versions" in process &&
!!process.versions?.node;
const _process = typeof process !== "undefined" ? process : undefined;
const isNode = typeof _process !== "undefined" && typeof _process.versions?.node === "string";
if (isNode) {
return {
type: "node",
version: process.versions.node,
parsedVersion: Number(process.versions.node.split(".")[0]),
version: _process.versions.node,
parsedVersion: Number(_process.versions.node.split(".")[0]),
};
}

Expand Down
10 changes: 5 additions & 5 deletions src/management/tests/mock-server/mockEndpointBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { type DefaultBodyType, type HttpHandler, HttpResponse, type HttpResponse

import { url } from "../../core";
import { toJson } from "../../core/json";
import { withFormUrlEncoded } from "./withFormUrlEncoded";
import { type WithFormUrlEncodedOptions, withFormUrlEncoded } from "./withFormUrlEncoded";
import { withHeaders } from "./withHeaders";
import { withJson, type WithJsonOptions } from "./withJson";
import { type WithJsonOptions, withJson } from "./withJson";

type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";

Expand All @@ -27,7 +27,7 @@ interface RequestHeadersStage extends RequestBodyStage, ResponseStage {

interface RequestBodyStage extends ResponseStage {
jsonBody(body: unknown, options?: WithJsonOptions): ResponseStage;
formUrlEncodedBody(body: unknown): ResponseStage;
formUrlEncodedBody(body: unknown, options?: WithFormUrlEncodedOptions): ResponseStage;
}

interface ResponseStage {
Expand Down Expand Up @@ -138,13 +138,13 @@ class RequestBuilder implements MethodStage, RequestHeadersStage, RequestBodySta
return this;
}

formUrlEncodedBody(body: unknown): ResponseStage {
formUrlEncodedBody(body: unknown, options?: WithFormUrlEncodedOptions): ResponseStage {
if (body === undefined) {
throw new Error(
"Undefined is not valid for form-urlencoded. Do not call formUrlEncodedBody if you want an empty body.",
);
}
this.predicates.push((resolver) => withFormUrlEncoded(body, resolver));
this.predicates.push((resolver) => withFormUrlEncoded(body, resolver, options));
return this;
}

Expand Down
19 changes: 17 additions & 2 deletions src/management/tests/mock-server/withFormUrlEncoded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@ import { type HttpResponseResolver, passthrough } from "msw";

import { toJson } from "../../core/json";

export interface WithFormUrlEncodedOptions {
/**
* List of field names to ignore when comparing request bodies.
* This is useful for pagination cursor fields that change between requests.
*/
ignoredFields?: string[];
}

/**
* Creates a request matcher that validates if the request form-urlencoded body exactly matches the expected object
* @param expectedBody - The exact body object to match against
* @param resolver - Response resolver to execute if body matches
* @param options - Optional configuration including fields to ignore
*/
export function withFormUrlEncoded(expectedBody: unknown, resolver: HttpResponseResolver): HttpResponseResolver {
export function withFormUrlEncoded(
expectedBody: unknown,
resolver: HttpResponseResolver,
options?: WithFormUrlEncodedOptions,
): HttpResponseResolver {
const ignoredFields = options?.ignoredFields ?? [];
return async (args) => {
const { request } = args;

Expand Down Expand Up @@ -41,7 +55,8 @@ export function withFormUrlEncoded(expectedBody: unknown, resolver: HttpResponse
}

const mismatches = findMismatches(actualBody, expectedBody);
if (Object.keys(mismatches).length > 0) {
const filteredMismatches = Object.keys(mismatches).filter((key) => !ignoredFields.includes(key));
if (filteredMismatches.length > 0) {
console.error("Form-urlencoded body mismatch:", toJson(mismatches, undefined, 2));
return passthrough();
}
Expand Down
106 changes: 105 additions & 1 deletion src/management/tests/unit/fetcher/makeRequest.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { makeRequest } from "../../../../../src/management/core/fetcher/makeRequest";
import {
makeRequest,
isCacheNoStoreSupported,
resetCacheNoStoreSupported,
} from "../../../src/management/core/fetcher/makeRequest";

describe("Test makeRequest", () => {
const mockPostUrl = "https://httpbin.org/post";
Expand All @@ -11,6 +15,7 @@ describe("Test makeRequest", () => {
beforeEach(() => {
mockFetch = jest.fn();
mockFetch.mockResolvedValue(new Response(JSON.stringify({ test: "successful" }), { status: 200 }));
resetCacheNoStoreSupported();
});

it("should handle POST request correctly", async () => {
Expand Down Expand Up @@ -50,4 +55,103 @@ describe("Test makeRequest", () => {
expect(calledOptions.signal).toBeDefined();
expect(calledOptions.signal).toBeInstanceOf(AbortSignal);
});

it("should not include cache option when disableCache is not set", async () => {
await makeRequest(mockFetch, mockGetUrl, "GET", mockHeaders, undefined);
const [, calledOptions] = mockFetch.mock.calls[0];
expect(calledOptions.cache).toBeUndefined();
});

it("should not include cache option when disableCache is false", async () => {
await makeRequest(
mockFetch,
mockGetUrl,
"GET",
mockHeaders,
undefined,
undefined,
undefined,
undefined,
undefined,
false,
);
const [, calledOptions] = mockFetch.mock.calls[0];
expect(calledOptions.cache).toBeUndefined();
});

it("should include cache: no-store when disableCache is true and runtime supports it", async () => {
// In Node.js test environment, Request supports the cache option
expect(isCacheNoStoreSupported()).toBe(true);
await makeRequest(
mockFetch,
mockGetUrl,
"GET",
mockHeaders,
undefined,
undefined,
undefined,
undefined,
undefined,
true,
);
const [, calledOptions] = mockFetch.mock.calls[0];
expect(calledOptions.cache).toBe("no-store");
});

it("should cache the result of isCacheNoStoreSupported", () => {
const first = isCacheNoStoreSupported();
const second = isCacheNoStoreSupported();
expect(first).toBe(second);
});

it("should reset cache detection state with resetCacheNoStoreSupported", () => {
// First call caches the result
const first = isCacheNoStoreSupported();
expect(first).toBe(true);

// Reset clears the cache
resetCacheNoStoreSupported();

// After reset, it should re-detect (and still return true in Node.js)
const second = isCacheNoStoreSupported();
expect(second).toBe(true);
});

it("should not include cache option when runtime does not support it (e.g. Cloudflare Workers)", async () => {
// Mock Request constructor to throw when cache option is passed,
// simulating runtimes like Cloudflare Workers
const OriginalRequest = globalThis.Request;
globalThis.Request = class MockRequest {
constructor(_url: string, init?: RequestInit) {
if (init?.cache != null) {
throw new TypeError("The 'cache' field on 'RequestInitializerDict' is not implemented.");
}
}
} as unknown as typeof Request;

try {
// Reset so the detection runs fresh with the mocked Request
resetCacheNoStoreSupported();
expect(isCacheNoStoreSupported()).toBe(false);

await makeRequest(
mockFetch,
mockGetUrl,
"GET",
mockHeaders,
undefined,
undefined,
undefined,
undefined,
undefined,
true,
);
const [, calledOptions] = mockFetch.mock.calls[0];
expect(calledOptions.cache).toBeUndefined();
} finally {
// Restore original Request
globalThis.Request = OriginalRequest;
resetCacheNoStoreSupported();
}
});
});
Loading