Skip to content

Commit

Permalink
feat(backend): Introduce createIsomorphicRequest (#1393)
Browse files Browse the repository at this point in the history
* feat(backend): Introduce createIsomorphicRequest

Introducing the new utility `createIsomorphicRequest` for the `@clerk/backend` package, so that the `authenticateRequest` signature will be more simplified, and it will be easier to integrate with more frameworks.

* fix(backend): Align build scripts

* fix(backend): Correctly export fetch apis using subpath imports

* feat(backend): Expose callback with Request and Headers as a param of createIsomorphicRequest

chore(backend): Add changeset for `createIsomorphicRequest`

refactor(clerk-sdk-node,backend): Remove sdk-node cookie dependency

Also refactor authenticateRequest options

fix(backend): Refactor the new IsomorphicRequest utilities

Also, manipulate headers objects to be compatible with Headers constructor

chore(repo): Revert `package-lock.json` changes

---------

Co-authored-by: Nikos Douvlis <nikosdouvlis@gmail.com>
  • Loading branch information
anagstef and nikosdouvlis authored Jul 7, 2023
1 parent 949959f commit fd692af
Show file tree
Hide file tree
Showing 26 changed files with 350 additions and 180 deletions.
12 changes: 12 additions & 0 deletions .changeset/funny-melons-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'gatsby-plugin-clerk': minor
'@clerk/clerk-sdk-node': minor
'@clerk/backend': minor
'@clerk/fastify': minor
'@clerk/nextjs': minor
'@clerk/remix': minor
---

Introduce `createIsomorphicRequest` in `@clerk/backend`

This utility simplifies the `authenticateRequest` signature, and it makes it easier to integrate with more frameworks.
39 changes: 22 additions & 17 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,31 @@
"types": "./dist/types/index.d.ts",
"main": "./dist/index.js",
"module": "./dist/esm/index.js",
"imports": {
"#crypto": {
"edge-light": "./dist/runtime/browser/crypto.mjs",
"worker": "./dist/runtime/browser/crypto.mjs",
"browser": "./dist/runtime/browser/crypto.mjs",
"node": "./dist/runtime/node/crypto.js",
"default": "./dist/runtime/browser/crypto.mjs"
},
"#fetch": {
"edge-light": "./dist/runtime/browser/fetch.mjs",
"worker": "./dist/runtime/browser/fetch.mjs",
"browser": "./dist/runtime/browser/fetch.mjs",
"node": "./dist/runtime/node/fetch.js",
"default": "./dist/runtime/browser/fetch.mjs"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"build": "run-s clean build:runtime build:lib",
"dev:publish": "npm run dev -- --env.publish",
"build:declarations": "tsc -p tsconfig.declarations.json",
"publish:local": "npx yalc push --replace --sig",
"build:lib": "tsup --env.NODE_ENV production",
"build:tests": "tsc -p tsconfig.test.json",
"build:runtime": "rsync -r --include '*/' --include '*.js' --include '*.mjs' --exclude='*' src/runtime dist",
Expand All @@ -28,6 +47,7 @@
"@clerk/types": "^3.46.1",
"@peculiar/webcrypto": "1.4.1",
"@types/node": "16.18.6",
"cookie": "0.5.0",
"deepmerge": "4.2.2",
"node-fetch-native": "1.0.1",
"snakecase-keys": "5.4.4",
Expand All @@ -36,6 +56,7 @@
"devDependencies": {
"@cloudflare/workers-types": "^3.18.0",
"@types/chai": "^4.3.3",
"@types/cookie": "^0.5.1",
"@types/qunit": "^2.19.3",
"@types/sinon": "^10.0.13",
"chai": "^4.3.6",
Expand All @@ -53,22 +74,6 @@
"publishConfig": {
"access": "public"
},
"imports": {
"#crypto": {
"edge-light": "./dist/runtime/browser/crypto.mjs",
"worker": "./dist/runtime/browser/crypto.mjs",
"browser": "./dist/runtime/browser/crypto.mjs",
"node": "./dist/runtime/node/crypto.js",
"default": "./dist/runtime/browser/crypto.mjs"
},
"#fetch": {
"edge-light": "./dist/runtime/browser/fetch.mjs",
"worker": "./dist/runtime/browser/fetch.mjs",
"browser": "./dist/runtime/browser/fetch.mjs",
"node": "./dist/runtime/node/fetch.js",
"default": "./dist/runtime/browser/fetch.mjs"
}
},
"homepage": "https://clerk.com/",
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default (QUnit: QUnit) => {
'Verification',
'constants',
'createAuthenticateRequest',
'createIsomorphicRequest',
'debugRequestState',
'decodeJwt',
'deserialize',
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { createBackendApiClient } from './api';
import type { CreateAuthenticateRequestOptions } from './tokens';
import { createAuthenticateRequest } from './tokens';

export { createIsomorphicRequest } from './util/IsomorphicRequest';

export * from './api/resources';
export * from './tokens';
export * from './tokens/jwt';
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/runtime/browser/fetch.mjs
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export default fetch;
export const RuntimeBlob = Blob;
export const RuntimeFormData = FormData;
export const RuntimeHeaders = Headers;
export const RuntimeRequest = Request;
export const RuntimeResponse = Response;
export const RuntimeAbortController = AbortController;
31 changes: 28 additions & 3 deletions packages/backend/src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,27 @@
// @ts-expect-error
import crypto from '#crypto';
// @ts-expect-error
import fetch from '#fetch';
import * as fetchApisPolyfill from '#fetch';

const {
default: fetch,
RuntimeAbortController,
RuntimeBlob,
RuntimeFormData,
RuntimeHeaders,
RuntimeRequest,
RuntimeResponse,
} = fetchApisPolyfill;

type Runtime = {
crypto: Crypto;
fetch: typeof global.fetch;
AbortController: typeof global.AbortController;
Blob: typeof global.Blob;
FormData: typeof global.FormData;
Headers: typeof global.Headers;
Request: typeof global.Request;
Response: typeof global.Response;
};

// Invoking the global.fetch without binding it first to the globalObject fails in
Expand All @@ -29,8 +45,17 @@ type Runtime = {
//
// https://github.com/supabase/supabase/issues/4417
const globalFetch = fetch.bind(globalThis);

// DO NOT CHANGE: Runtime needs to be imported as a default export so that we can stub its dependencies with Sinon.js
// For more information refer to https://sinonjs.org/how-to/stub-dependency/
const runtime: Runtime = { crypto, fetch: globalFetch };
const runtime: Runtime = {
crypto,
fetch: globalFetch,
AbortController: RuntimeAbortController,
Blob: RuntimeBlob,
FormData: RuntimeFormData,
Headers: RuntimeHeaders,
Request: RuntimeRequest,
Response: RuntimeResponse,
};

export default runtime;
9 changes: 8 additions & 1 deletion packages/backend/src/runtime/node/fetch.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
const fetch = require('node-fetch-native');
const { fetch, Blob, FormData, Headers, Request, Response, AbortController } = require('node-fetch-native');

module.exports = fetch;
module.exports.RuntimeBlob = Blob;
module.exports.RuntimeFormData = FormData;
module.exports.RuntimeHeaders = Headers;
module.exports.RuntimeRequest = Request;
module.exports.RuntimeResponse = Response;
module.exports.RuntimeAbortController = AbortController;
11 changes: 4 additions & 7 deletions packages/backend/src/tokens/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
export * from './authObjects';
export * from './factory';
export { RequestState, AuthStatus } from './authStatus';
export type { RequestState } from './authStatus';
export { AuthStatus } from './authStatus';
export { loadInterstitialFromLocal } from './interstitial';
export {
debugRequestState,
AuthenticateRequestOptions,
OptionalVerifyTokenOptions,
RequiredVerifyTokenOptions,
} from './request';
export type { AuthenticateRequestOptions, OptionalVerifyTokenOptions, RequiredVerifyTokenOptions } from './request';
export { debugRequestState } from './request';
27 changes: 22 additions & 5 deletions packages/backend/src/tokens/request.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { API_URL, API_VERSION } from '../constants';
import { API_URL, API_VERSION, constants } from '../constants';
import { assertValidSecretKey } from '../util/assertValidSecretKey';
import { isDevelopmentFromApiKey } from '../util/instance';
import { buildRequest, stripAuthorizationHeader } from '../util/IsomorphicRequest';
import { parsePublishableKey } from '../util/parsePublishableKey';
import type { RequestState } from './authStatus';
import { AuthErrorReason, interstitial, signedOut, unknownState } from './authStatus';
Expand Down Expand Up @@ -100,6 +101,7 @@ export type AuthenticateRequestOptions = OptionalVerifyTokenOptions &
* @experimental
*/
signInUrl?: string;
request?: Request;
};

function assertSignInUrlExists(signInUrl: string | undefined, key: string): asserts signInUrl is string {
Expand All @@ -115,10 +117,25 @@ function assertProxyUrlOrDomain(proxyUrlOrDomain: string | undefined) {
}

export async function authenticateRequest(options: AuthenticateRequestOptions): Promise<RequestState> {
options.frontendApi = parsePublishableKey(options.publishableKey)?.frontendApi || options.frontendApi || '';
options.apiUrl = options.apiUrl || API_URL;
options.apiVersion = options.apiVersion || API_VERSION;
options.headerToken = options.headerToken?.replace('Bearer ', '');
const { cookies, headers, searchParams } = buildRequest(options?.request);

options = {
...options,
frontendApi: parsePublishableKey(options.publishableKey)?.frontendApi || options.frontendApi,
apiUrl: options.apiUrl || API_URL,
apiVersion: options.apiVersion || API_VERSION,
headerToken: stripAuthorizationHeader(options.headerToken || headers?.(constants.Headers.Authorization)),
cookieToken: options.cookieToken || cookies?.(constants.Cookies.Session),
clientUat: options.clientUat || cookies?.(constants.Cookies.ClientUat),
origin: options.origin || headers?.(constants.Headers.Origin),
host: options.host || headers?.(constants.Headers.Host),
forwardedHost: options.forwardedHost || headers?.(constants.Headers.ForwardedHost),
forwardedPort: options.forwardedPort || headers?.(constants.Headers.ForwardedPort),
forwardedProto: options.forwardedProto || headers?.(constants.Headers.ForwardedProto),
referrer: options.referrer || headers?.(constants.Headers.Referrer),
userAgent: options.userAgent || headers?.(constants.Headers.UserAgent),
searchParams: options.searchParams || searchParams || undefined,
};

assertValidSecretKey(options.secretKey || options.apiKey);

Expand Down
49 changes: 49 additions & 0 deletions packages/backend/src/util/IsomorphicRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { parse } from 'cookie';

import runtime from '../runtime';

type IsomorphicRequestOptions = (Request: typeof runtime.Request, Headers: typeof runtime.Headers) => Request;
export const createIsomorphicRequest = (cb: IsomorphicRequestOptions): Request => {
return cb(runtime.Request, runtime.Headers);
};

export const buildRequest = (req?: Request) => {
if (!req) {
return {};
}
const cookies = parseIsomorphicRequestCookies(req);
const headers = getHeaderFromIsomorphicRequest(req);
const searchParams = getSearchParamsFromIsomorphicRequest(req);

return {
cookies,
headers,
searchParams,
};
};

const decode = (str: string): string => {
if (!str) {
return str;
}
return str.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent);
};

const parseIsomorphicRequestCookies = (req: Request) => {
const cookies = req.headers && req.headers?.get('cookie') ? parse(req.headers.get('cookie') as string) : {};
return (key: string): string | undefined => {
const value = cookies?.[key];
if (value === undefined) {
return undefined;
}
return decode(value);
};
};

const getHeaderFromIsomorphicRequest = (req: Request) => (key: string) => req?.headers?.get(key) || undefined;

const getSearchParamsFromIsomorphicRequest = (req: Request) => (req?.url ? new URL(req.url)?.searchParams : undefined);

export const stripAuthorizationHeader = (authValue: string | undefined | null): string | undefined => {
return authValue?.replace('Bearer ', '');
};
12 changes: 12 additions & 0 deletions packages/backend/tsconfig.declarations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"skipLibCheck": true,
"noEmit": false,
"declaration": true,
"emitDeclarationOnly": true,
"declarationMap": true,
"sourceMap": false,
"declarationDir": "./dist/types"
}
}
18 changes: 9 additions & 9 deletions packages/backend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"baseUrl": "src",
"baseUrl": ".",
"declaration": true,
"declarationDir": "dist/types",
"declarationMap": true,
"emitDeclarationOnly": true,
"declarationMap": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
"moduleResolution": "node",
"noUnusedLocals": true,
"noImplicitReturns": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"removeComments": true,
"outDir": "dist",
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": false,
"strict": true,
"target": "ES2020"
"target": "ES2020",
"isolatedModules": true
},
"include": ["src", "global.d.ts"],
"exclude": ["node_modules", "dist", "/src/runtime/*", "src/**/*.spec.ts", "src/**/*.test.ts", "src/__tests__"]
"include": ["src"],
"exclude": ["node_modules", "dist", "/src/runtime/*", "src/**/*.spec.ts", "src/**/*.test.ts", "src/tests"]
}
35 changes: 29 additions & 6 deletions packages/backend/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,45 @@
import type { Options } from 'tsup';
import { defineConfig } from 'tsup';

import { runAfterLast } from '../../scripts/utils';
// @ts-ignore
import { name, version } from './package.json';

export default defineConfig(overrideOptions => {
const isProd = overrideOptions.env?.NODE_ENV === 'production';
const isWatch = !!overrideOptions.watch;
const shouldPublish = !!overrideOptions.env?.publish;

return {
// const onSuccess = (format: 'esm' | 'cjs') => {
// const rsync = `rsync -r --include '*/' --include '*.js' --include '*.mjs' --include '*.cjs' --exclude='*' ./src/runtime ./dist/${format}/`;
// // return `cp ./package.${format}.json ./dist/${format}/package.json && ${rsync}`;
// return rsync;
// };

const common: Options = {
entry: ['src/index.ts'],
onSuccess: 'tsc && npm run build:runtime',
minify: isProd,
onSuccess: `rsync -r --include '*/' --include '*.js' --include '*.mjs' --include '*.cjs' --exclude='*' ./src/runtime ./dist/`,
sourcemap: true,
format: ['cjs', 'esm'],
define: {
PACKAGE_NAME: `"${name}"`,
PACKAGE_VERSION: `"${version}"`,
__DEV__: `${!isProd}`,
__DEV__: `${isWatch}`,
},
external: ['#crypto', '#fetch'],
legacyOutput: true,
bundle: true,
clean: true,
minify: false,
};

const esm: Options = {
...common,
format: 'esm',
};

const cjs: Options = {
...common,
format: 'cjs',
};

return runAfterLast(['npm run build:declarations', shouldPublish && 'npm run publish:local'])(esm, cjs);
});
Loading

0 comments on commit fd692af

Please sign in to comment.