-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support Cloudflare workers/Edge runtime environments (#962)
## Description This PR aims to make the node SDK (will need to be renamed to JS SDK maybe?) compatible with Cloudflare workers / Edge runtime environments. This is achieved by: - Removing `axios` and using native `fetch` instead - Removing usage of Node's `crypto` module in favour for native web `crypto` Here are the important changes: - Swapping out axios's client for [our own custom fetch-based client](https://github.com/workos/workos-node/pull/962/files#diff-32c7ee5e94c4dc53afedbcdb133765b9ebfeee5922e4b6a1fefd0987ee0d86d8) in [the main `WorkOS` class](https://github.com/workos/workos-node/pull/962/files#diff-43fae42284da1898f98b43b56a7bc6f5fb979e271bca59f4670751ba2c0020ef) - Rewrite the webhooks APIs using web `crypto` in [the `Webhooks` class](https://github.com/workos/workos-node/pull/962/files#diff-8b772e2b2baa9169ed0a1e9039df9b4b4b353690593830c843dff21f3dc525f7) Most of the rest of the changes are rewriting the tests to ensure they all pass because of the changes. ## Documentation Does this require changes to the WorkOS Docs? E.g. the [API Reference](https://workos.com/docs/reference) or code snippets need updates. ``` [x] Yes ``` If yes, link a related docs PR and add a docs maintainer as a reviewer. Their approval is required. > [!WARNING] > TODO: Add Docs PR
- Loading branch information
1 parent
5260da3
commit c38dd7d
Showing
26 changed files
with
839 additions
and
766 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { enableFetchMocks } from 'jest-fetch-mock'; | ||
import { Crypto } from '@peculiar/webcrypto'; | ||
|
||
enableFetchMocks(); | ||
|
||
global.crypto = new Crypto(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,6 @@ | ||
import { AxiosRequestConfig } from 'axios'; | ||
|
||
export interface WorkOSOptions { | ||
apiHostname?: string; | ||
https?: boolean; | ||
port?: number; | ||
axios?: Omit<AxiosRequestConfig, 'baseURL'>; | ||
config?: RequestInit; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { FetchError } from './fetch-error'; | ||
|
||
export class FetchClient { | ||
constructor(readonly baseURL: string, readonly options?: RequestInit) {} | ||
|
||
async get( | ||
path: string, | ||
options: { params?: Record<string, any>; headers?: HeadersInit }, | ||
) { | ||
const resourceURL = this.getResourceURL(path, options.params); | ||
const response = await this.fetch(resourceURL, { | ||
headers: options.headers, | ||
}); | ||
return { data: await response.json() }; | ||
} | ||
|
||
async post<Entity = any>( | ||
path: string, | ||
entity: Entity, | ||
options: { params?: Record<string, any>; headers?: HeadersInit }, | ||
) { | ||
const resourceURL = this.getResourceURL(path, options.params); | ||
const bodyIsSearchParams = entity instanceof URLSearchParams; | ||
const contentTypeHeader = bodyIsSearchParams | ||
? { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' } | ||
: undefined; | ||
const body = bodyIsSearchParams ? entity : JSON.stringify(entity); | ||
const response = await this.fetch(resourceURL, { | ||
method: 'POST', | ||
headers: { ...contentTypeHeader, ...options.headers }, | ||
body, | ||
}); | ||
return { data: await response.json() }; | ||
} | ||
|
||
async put<Entity = any>( | ||
path: string, | ||
entity: Entity, | ||
options: { params?: Record<string, any>; headers?: HeadersInit }, | ||
) { | ||
const resourceURL = this.getResourceURL(path, options.params); | ||
const response = await this.fetch(resourceURL, { | ||
method: 'PUT', | ||
headers: options.headers, | ||
body: JSON.stringify(entity), | ||
}); | ||
return { data: await response.json() }; | ||
} | ||
|
||
async delete( | ||
path: string, | ||
options: { params?: Record<string, any>; headers?: HeadersInit }, | ||
) { | ||
const resourceURL = this.getResourceURL(path, options.params); | ||
await this.fetch(resourceURL, { | ||
method: 'DELETE', | ||
headers: options.headers, | ||
}); | ||
} | ||
|
||
private getResourceURL(path: string, params?: Record<string, any>) { | ||
const queryString = getQueryString(params); | ||
const url = new URL( | ||
[path, queryString].filter(Boolean).join('?'), | ||
this.baseURL, | ||
); | ||
return url.toString(); | ||
} | ||
|
||
private async fetch(url: string, options?: RequestInit) { | ||
const response = await fetch(url, { | ||
...this.options, | ||
...options, | ||
headers: { | ||
Accept: 'application/json, text/plain, */*', | ||
'Content-Type': 'application/json', | ||
...this.options?.headers, | ||
...options?.headers, | ||
}, | ||
}); | ||
|
||
if (!response.ok) { | ||
throw new FetchError({ | ||
message: response.statusText, | ||
response: { | ||
status: response.status, | ||
headers: response.headers, | ||
data: await response.json(), | ||
}, | ||
}); | ||
} | ||
|
||
return response; | ||
} | ||
} | ||
|
||
function getQueryString(queryObj?: Record<string, any>) { | ||
if (!queryObj) return undefined; | ||
|
||
const sanitizedQueryObj: Record<string, any> = {}; | ||
|
||
Object.entries(queryObj).forEach(([param, value]) => { | ||
if (value !== '' && value !== undefined) sanitizedQueryObj[param] = value; | ||
}); | ||
|
||
return new URLSearchParams(sanitizedQueryObj).toString(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
export class FetchError<T> extends Error { | ||
readonly name: string = 'FetchError'; | ||
readonly message: string = 'The request could not be completed.'; | ||
readonly response: { status: number; headers: Headers; data: T }; | ||
|
||
constructor({ | ||
message, | ||
response, | ||
}: { | ||
message: string; | ||
readonly response: FetchError<T>['response']; | ||
}) { | ||
super(message); | ||
this.message = message; | ||
this.response = response; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import fetch, { MockParams } from 'jest-fetch-mock'; | ||
|
||
export function fetchOnce( | ||
response: any = {}, | ||
{ status = 200, ...rest }: MockParams = {}, | ||
) { | ||
return fetch.once(JSON.stringify(response), { status, ...rest }); | ||
} | ||
|
||
export function fetchURL() { | ||
return fetch.mock.calls[0][0]; | ||
} | ||
|
||
export function fetchSearchParams() { | ||
return Object.fromEntries(new URL(String(fetchURL())).searchParams); | ||
} | ||
|
||
export function fetchHeaders() { | ||
return fetch.mock.calls[0][1]?.headers; | ||
} | ||
|
||
export function fetchBody() { | ||
const body = fetch.mock.calls[0][1]?.body; | ||
if (body instanceof URLSearchParams) { | ||
return body.toString(); | ||
} | ||
return JSON.parse(String(body)); | ||
} |
Oops, something went wrong.