Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export class Versions {
request: Management.actions.ListActionVersionsRequestParameters = {},
requestOptions?: Versions.RequestOptions,
): Promise<core.Page<Management.ActionVersion>> {
// This variable is populated from the Open API specification file.
const endpointScopes = ["read:actions_versions"];

const list = core.HttpResponsePromise.interceptFunction(
async (
request: Management.actions.ListActionVersionsRequestParameters,
Expand All @@ -82,14 +85,18 @@ export class Versions {
method: "GET",
headers: mergeHeaders(
this._options?.headers,
mergeOnlyDefinedHeaders({ Authorization: await this._getAuthorizationHeader() }),
// Here we pass the scope for the endpoint to the Supplier to ensure the scope is used when calling the token supplier.
mergeOnlyDefinedHeaders({ Authorization: await this._getAuthorizationHeader(endpointScopes) }),
requestOptions?.headers,
),
queryParameters: { ..._queryParams, ...requestOptions?.queryParams },
timeoutMs:
requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000,
maxRetries: requestOptions?.maxRetries,
abortSignal: requestOptions?.abortSignal,
// Here we pass the scope for the endpoint to the fetcher, this enables the fetcher to take full control over the authorization aspect.
// Doing so enables advanced use-cases like DPoP as well as token renewal when the token is expired.
scope: endpointScopes,
});
if (_response.ok) {
return {
Expand Down Expand Up @@ -348,7 +355,12 @@ export class Versions {
}
}

protected async _getAuthorizationHeader(): Promise<string> {
return `Bearer ${await core.Supplier.get(this._options.token)}`;
// Marked scope as optional to avoid having to update every snippet for demonstrating the changes.
protected async _getAuthorizationHeader(scope?: string[] | undefined): Promise<string> {
const token = await core.Supplier.get(this._options.token, { scope });

// Ensure to not add an empty Bearer token.
// Doing this ensures the header is filtered out by `mergeOnlyDefinedHeaders`.
return token && `Bearer ${token}`;
}
}
1 change: 1 addition & 0 deletions src/management/core/fetcher/Fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export declare namespace Fetcher {
requestType?: "json" | "file" | "bytes";
responseType?: "json" | "blob" | "sse" | "streaming" | "text" | "arrayBuffer" | "binary-response";
duplex?: "half";
scope?: string[];
}

export type Error = FailedStatusCodeError | NonJsonError | TimeoutError | UnknownError;
Expand Down
9 changes: 6 additions & 3 deletions src/management/core/fetcher/Supplier.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
export type Supplier<T> = T | Promise<T> | (() => T | Promise<T>);
export type SupplierContext = { scope: string[] | undefined };
export type SupplierFn<T> = (context?: SupplierContext) => T | Promise<T>;
export type Supplier<T> = T | Promise<T> | SupplierFn<T>;

export const Supplier = {
get: async <T>(supplier: Supplier<T>): Promise<T> => {
// Marked context as optional to not have to update all snippets.
get: async <T>(supplier: Supplier<T>, context?: SupplierContext): Promise<T> => {
if (typeof supplier === "function") {
return (supplier as () => T)();
return (supplier as SupplierFn<T>)(context);
} else {
return supplier;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ManagementClient } from "../../wrapper/ManagementClient.js";

describe("ManagementClient with Scope-Aware Supplier and Fetcher", () => {
it("handle custom changes to demonstrate", async () => {
const consoleSpy = jest.spyOn(console, "log").mockImplementation();

const client = new ManagementClient({
domain: "your-tenant.auth0.com",
clientId: "your-client-id",
clientSecret: "your-client-secret",
token: (context) => {
console.log(`Scope passed to token supplier: ${context?.scope?.join(", ")}`);
return "your-static-token";
},
fetcher: async (args) => {
console.log(`Scope passed to fetcher: ${args.scope?.join(", ")}`);
const response = await fetch(args.url, {
method: args.method,
headers: args.headers as Record<string, string>,
body: args.body ? JSON.stringify(args.body) : undefined,
});

if (response.ok) {
return {
ok: true as const,
body: await response.json(),
headers: response.headers,
rawResponse: response,
};
} else {
return {
ok: false as const,
error: {
reason: "status-code" as const,
statusCode: response.status,
body: await response.text(),
},
rawResponse: response,
};
}
},
});

try {
await client.actions.versions.list("action123");
} catch {}

expect(consoleSpy).toHaveBeenNthCalledWith(1, "Scope passed to token supplier: read:actions_versions");
expect(consoleSpy).toHaveBeenNthCalledWith(2, "Scope passed to fetcher: read:actions_versions");
});
});
Loading