From ff74d110334b4ac8175c92f3e1b6b2318ba35587 Mon Sep 17 00:00:00 2001 From: Alex MacCaw Date: Fri, 18 Aug 2023 15:17:20 -0400 Subject: [PATCH] tsup ify (#1) * tsup ify * lint --- .github/workflows/pull_request.yml | 2 +- .github/workflows/pull_request_lint.yml | 2 +- .prettierrc | 6 + README.md | 31 +- core/error.ts | 19 - google/accessToken.test.ts | 36 -- google/accessToken.ts | 199 --------- google/customToken.test.ts | 76 ---- google/index.ts | 7 - index.ts | 5 - jest.config.js | 30 +- package.json | 21 +- {core => src}/env.ts | 2 +- src/error.ts | 16 + src/google/accessToken.test.ts | 32 ++ src/google/accessToken.ts | 197 ++++++++ {google => src/google}/credentials.test.ts | 38 +- {google => src/google}/credentials.ts | 94 ++-- src/google/customToken.test.ts | 70 +++ {google => src/google}/customToken.ts | 42 +- {google => src/google}/idToken.test.ts | 50 +-- {google => src/google}/idToken.ts | 197 ++++---- src/google/index.ts | 7 + src/index.ts | 1 + {core => src}/jwt.test.ts | 26 +- {core => src}/jwt.ts | 91 ++-- {core => src}/utils.ts | 8 +- test/env.ts | 4 +- test/setup.ts | 6 +- tsup.config.ts | 8 + yarn.lock | 495 ++++++++++++++++++++- 31 files changed, 1136 insertions(+), 682 deletions(-) create mode 100644 .prettierrc delete mode 100644 core/error.ts delete mode 100644 google/accessToken.test.ts delete mode 100644 google/accessToken.ts delete mode 100644 google/customToken.test.ts delete mode 100644 google/index.ts delete mode 100644 index.ts rename {core => src}/env.ts (74%) create mode 100644 src/error.ts create mode 100644 src/google/accessToken.test.ts create mode 100644 src/google/accessToken.ts rename {google => src/google}/credentials.test.ts (60%) rename {google => src/google}/credentials.ts (60%) create mode 100644 src/google/customToken.test.ts rename {google => src/google}/customToken.ts (72%) rename {google => src/google}/idToken.test.ts (64%) rename {google => src/google}/idToken.ts (64%) create mode 100644 src/google/index.ts create mode 100644 src/index.ts rename {core => src}/jwt.test.ts (85%) rename {core => src}/jwt.ts (60%) rename {core => src}/utils.ts (59%) create mode 100644 tsup.config.ts diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ee92e69..d610738 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 19 - cache: "yarn" + cache: 'yarn' # Install dependencies - run: yarn config set enableGlobalCache false diff --git a/.github/workflows/pull_request_lint.yml b/.github/workflows/pull_request_lint.yml index a48db76..8d0c728 100644 --- a/.github/workflows/pull_request_lint.yml +++ b/.github/workflows/pull_request_lint.yml @@ -1,7 +1,7 @@ # GitHub Actions workflow # https://help.github.com/actions -name: "conventionalcommits.org" +name: 'conventionalcommits.org' on: pull_request: diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..682cf09 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +printWidth: 90 +semi: false +trailingComma: 'all' +singleQuote: true +jsxBracketSameLine: false +arrowParens: 'always' diff --git a/README.md b/README.md index 6743fd3..486a4a1 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,12 @@ $ yarn add web-auth-library **NOTE**: The `credentials` argument in the examples below is expected to be a serialized JSON string of a [Google Cloud service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys), `apiKey` is Google Cloud API Key (Firebase API Key), and `projectId` is a Google Cloud project ID. ```ts -import { verifyIdToken } from "web-auth-library/google"; +import { verifyIdToken } from 'web-auth-library/google' const token = await verifyIdToken({ idToken, credentials: env.GOOGLE_CLOUD_CREDENTIALS, -}); +}) // => { // iss: 'https://securetoken.google.com/example', @@ -57,32 +57,29 @@ const token = await verifyIdToken({ ### Create an access token for accessing [Google Cloud APIs](https://developers.google.com/apis-explorer) ```ts -import { getAccessToken } from "web-auth-library/google"; +import { getAccessToken } from 'web-auth-library/google' // Generate a short lived access token from the service account key credentials const accessToken = await getAccessToken({ credentials: env.GOOGLE_CLOUD_CREDENTIALS, - scope: "https://www.googleapis.com/auth/cloud-platform", -}); + scope: 'https://www.googleapis.com/auth/cloud-platform', +}) // Make a request to one of the Google's APIs using that token -const res = await fetch( - "https://cloudresourcemanager.googleapis.com/v1/projects", - { - headers: { Authorization: `Bearer ${accessToken}` }, - } -); +const res = await fetch('https://cloudresourcemanager.googleapis.com/v1/projects', { + headers: { Authorization: `Bearer ${accessToken}` }, +}) ``` ## Create a custom ID token using Service Account credentials ```ts -import { getIdToken } from "web-auth-library/google"; +import { getIdToken } from 'web-auth-library/google' const idToken = await getIdToken({ credentials: env.GOOGLE_CLOUD_CREDENTIALS, - audience: "https://example.com", -}); + audience: 'https://example.com', +}) ``` ## An alternative way passing credentials @@ -90,10 +87,10 @@ const idToken = await getIdToken({ Instead of passing credentials via `options.credentials` argument, you can also let the library pick up credentials from the list of environment variables using standard names such as `GOOGLE_CLOUD_CREDENTIALS`, `GOOGLE_CLOUD_PROJECT`, `FIREBASE_API_KEY`, for example: ```ts -import { verifyIdToken } from "web-auth-library/google"; +import { verifyIdToken } from 'web-auth-library/google' -const env = { GOOGLE_CLOUD_CREDENTIALS: "..." }; -const token = await verifyIdToken({ idToken, env }); +const env = { GOOGLE_CLOUD_CREDENTIALS: '...' } +const token = await verifyIdToken({ idToken, env }) ``` ## Optimize cache renewal background tasks diff --git a/core/error.ts b/core/error.ts deleted file mode 100644 index c9512d6..0000000 --- a/core/error.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* SPDX-FileCopyrightText: 2022-present Kriasoft */ -/* SPDX-License-Identifier: MIT */ - -export class FetchError extends Error { - readonly name: string = "FetchError"; - readonly response: Response; - - constructor( - message: string, - options: { response: Response; cause?: unknown } - ) { - super(message, { cause: options?.cause }); - this.response = options.response; - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, Error); - } - } -} diff --git a/google/accessToken.test.ts b/google/accessToken.test.ts deleted file mode 100644 index 23795f3..0000000 --- a/google/accessToken.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* SPDX-FileCopyrightText: 2022-present Kriasoft */ -/* SPDX-License-Identifier: MIT */ - -import { decodeJwt } from "jose"; -import env from "../test/env.js"; -import { getAccessToken } from "./accessToken.js"; - -test("getAccessToken({ credentials, scope })", async () => { - const accessToken = await getAccessToken({ - credentials: env.GOOGLE_CLOUD_CREDENTIALS, - scope: "https://www.googleapis.com/auth/cloud-platform", - }); - - expect(accessToken?.substring(0, 30)).toEqual( - expect.stringContaining("ya29.c.") - ); -}); - -test("getAccessToken({ credentials, audience })", async () => { - const idToken = await getAccessToken({ - credentials: env.GOOGLE_CLOUD_CREDENTIALS, - audience: "https://example.com", - }); - - expect(idToken?.substring(0, 30)).toEqual( - expect.stringContaining("eyJhbGciOi") - ); - - expect(decodeJwt(idToken)).toEqual( - expect.objectContaining({ - aud: "https://example.com", - email_verified: true, - iss: "https://accounts.google.com", - }) - ); -}); diff --git a/google/accessToken.ts b/google/accessToken.ts deleted file mode 100644 index 70a8423..0000000 --- a/google/accessToken.ts +++ /dev/null @@ -1,199 +0,0 @@ -/* SPDX-FileCopyrightText: 2022-present Kriasoft */ -/* SPDX-License-Identifier: MIT */ - -import { decodeJwt } from "jose"; -import { canUseDefaultCache } from "../core/env.js"; -import { FetchError } from "../core/error.js"; -import { logOnce } from "../core/utils.js"; -import { getCredentials, type Credentials } from "./credentials.js"; -import { createCustomToken } from "./customToken.js"; - -const defaultCache = new Map(); - -/** - * Fetches an access token from Google Cloud API using the provided - * service account credentials. - * - * @throws {FetchError} — If the access token could not be fetched. - */ -export async function getAccessToken(options: Options) { - if (!options?.waitUntil && canUseDefaultCache) { - logOnce("warn", "verifyIdToken", "Missing `waitUntil` option."); - } - - let credentials: Credentials; - - // Normalize service account credentials - // using env.GOOGLE_CLOUD_CREDENTIALS as a fallback - if (options?.credentials) { - credentials = getCredentials(options.credentials); - } else { - if (!options?.env?.GOOGLE_CLOUD_CREDENTIALS) { - throw new TypeError("Missing credentials"); - } - credentials = getCredentials(options.env.GOOGLE_CLOUD_CREDENTIALS); - } - - // Normalize authentication scope and audience values - const scope = Array.isArray(options.scope) - ? options.scope.join(",") - : options.scope; - const audience = Array.isArray(options.audience) - ? options.audience.join(",") - : options.audience; - - const tokenUrl = credentials.token_uri; - - // Create a cache key that can be used with Cloudflare Cache API - const cacheKeyUrl = new URL(tokenUrl); - cacheKeyUrl.searchParams.set("scope", scope ?? ""); - cacheKeyUrl.searchParams.set("aud", audience ?? ""); - cacheKeyUrl.searchParams.set("key", credentials.private_key_id); - const cacheKey = cacheKeyUrl.toString(); - - // Attempt to retrieve the token from the cache - const cache: Map = options.cache ?? defaultCache; - const cacheValue = cache.get(cacheKey); - let now = Math.floor(Date.now() / 1000); - - if (cacheValue) { - if (cacheValue.created > now - 60 * 60) { - let token = await cacheValue.promise; - - if (token.expires > now) { - return token.token; - } else { - const nextValue = cache.get(cacheKey); - - if (nextValue && nextValue !== cacheValue) { - token = await nextValue.promise; - if (token.expires > now) { - return token.token; - } else { - cache.delete(cacheKey); - } - } - } - } else { - cache.delete(cacheKey); - } - } - - const promise = (async () => { - let res: Response | undefined; - - // Attempt to retrieve the token from Cloudflare cache - // if the code is running in Cloudflare Workers environment - if (canUseDefaultCache) { - res = await caches.default.match(cacheKey); - } - - if (!res) { - now = Math.floor(Date.now() / 1000); - - // Request a new token from the Google Cloud API - const jwt = await createCustomToken({ - credentials, - scope: options.audience ?? options.scope, - }); - const body = new URLSearchParams(); - body.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"); - body.append("assertion", jwt); - res = await fetch(tokenUrl, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body, - }); - - if (!res.ok) { - const error = await res - .json<{ error_description?: string }>() - .then((data) => data?.error_description) - .catch(() => undefined); - throw new FetchError(error ?? "Failed to fetch an access token.", { - response: res, - }); - } - - if (canUseDefaultCache) { - let cacheRes = res.clone(); - cacheRes = new Response(cacheRes.body, cacheRes); - cacheRes.headers.set("Cache-Control", `max-age=3590, public`); - cacheRes.headers.set("Last-Modified", new Date().toUTCString()); - const cachePromise = caches.default.put(cacheKey, cacheRes); - - if (options.waitUntil) { - options.waitUntil(cachePromise); - } - } - } - - const data = await res.json(); - - if ("id_token" in data) { - const claims = decodeJwt(data.id_token); - return { token: data.id_token, expires: claims.exp as number }; - } - - const lastModified = res.headers.get("last-modified"); - const expires = lastModified - ? Math.floor(new Date(lastModified).valueOf() / 1000) + data.expires_in - : now + data.expires_in; - - return { expires, token: data.access_token }; - })(); - - cache.set(cacheKey, { created: now, promise }); - return await promise.then((data) => data.token); -} - -// #region Types - -type Options = { - /** - * Google Cloud service account credentials. - * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys - * @default env.GOOGLE_CLOUD_PROJECT - */ - credentials: Credentials | string; - /** - * Authentication scope(s). - */ - scope?: string[] | string; - /** - * Recipients that the ID token should be issued for. - */ - audience?: string[] | string; - env?: { - /** - * Google Cloud project ID. - */ - GOOGLE_CLOUD_PROJECT?: string; - /** - * Google Cloud service account credentials. - * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys - */ - GOOGLE_CLOUD_CREDENTIALS: string; - }; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - waitUntil?: (promise: Promise) => void; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - cache?: Map; -}; - -type TokenResponse = - | { - access_token: string; - expires_in: number; - token_type: string; - } - | { - id_token: string; - }; - -type CacheValue = { - created: number; - promise: Promise<{ token: string; expires: number }>; -}; - -// #endregion diff --git a/google/customToken.test.ts b/google/customToken.test.ts deleted file mode 100644 index 85c52de..0000000 --- a/google/customToken.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* SPDX-FileCopyrightText: 2022-present Kriasoft */ -/* SPDX-License-Identifier: MIT */ - -import { decodeJwt } from "jose"; -import env from "../test/env.js"; -import { createCustomToken } from "./customToken.js"; - -test("createCustomToken({ credentials, scope })", async () => { - const customToken = await createCustomToken({ - credentials: env.GOOGLE_CLOUD_CREDENTIALS, - scope: "https://www.example.com", - }); - - expect(customToken?.substring(0, 30)).toEqual( - expect.stringContaining("eyJhbGciOi") - ); - - expect(decodeJwt(customToken)).toEqual( - expect.objectContaining({ - iss: expect.stringMatching(/\.iam\.gserviceaccount\.com$/), - aud: "https://oauth2.googleapis.com/token", - scope: "https://www.example.com", - iat: expect.any(Number), - exp: expect.any(Number), - }) - ); -}); - -test("createCustomToken({ credentials, scope: scopes })", async () => { - const customToken = await createCustomToken({ - credentials: env.GOOGLE_CLOUD_CREDENTIALS, - scope: ["https://www.example.com", "https://beta.example.com"], - }); - - expect(customToken?.substring(0, 30)).toEqual( - expect.stringContaining("eyJhbGciOi") - ); - - expect(decodeJwt(customToken)).toEqual( - expect.objectContaining({ - iss: expect.stringMatching(/\.iam\.gserviceaccount\.com$/), - aud: "https://oauth2.googleapis.com/token", - scope: "https://www.example.com https://beta.example.com", - iat: expect.any(Number), - exp: expect.any(Number), - }) - ); -}); - -test("createCustomToken({ env, scope })", async () => { - const customToken = await createCustomToken({ - scope: "https://www.googleapis.com/auth/cloud-platform", - env: { GOOGLE_CLOUD_CREDENTIALS: env.GOOGLE_CLOUD_CREDENTIALS }, - }); - - expect(customToken?.substring(0, 30)).toEqual( - expect.stringContaining("eyJhbGciOi") - ); - - expect(decodeJwt(customToken)).toEqual( - expect.objectContaining({ - iss: expect.stringMatching(/\.iam\.gserviceaccount\.com$/), - aud: "https://oauth2.googleapis.com/token", - scope: "https://www.googleapis.com/auth/cloud-platform", - iat: expect.any(Number), - exp: expect.any(Number), - }) - ); -}); - -test("createCustomToken({ env, scope })", async () => { - const promise = createCustomToken({ - scope: "https://www.googleapis.com/auth/cloud-platform", - }); - expect(promise).rejects.toThrow(new TypeError("Missing credentials")); -}); diff --git a/google/index.ts b/google/index.ts deleted file mode 100644 index 1816552..0000000 --- a/google/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* SPDX-FileCopyrightText: 2022-present Kriasoft */ -/* SPDX-License-Identifier: MIT */ - -export * from "./accessToken.js"; -export * from "./credentials.js"; -export * from "./customToken.js"; -export * from "./idToken.js"; diff --git a/index.ts b/index.ts deleted file mode 100644 index 0070968..0000000 --- a/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* SPDX-FileCopyrightText: 2022-present Kriasoft */ -/* SPDX-License-Identifier: MIT */ - -export * as jwt from "./core/jwt.js"; -export * as google from "./google/index.js"; diff --git a/jest.config.js b/jest.config.js index 0b6dca4..54927d6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,33 +8,19 @@ * @type {import("@jest/types").Config.InitialOptions} */ export default { - testEnvironment: "miniflare", + testEnvironment: 'miniflare', - testPathIgnorePatterns: [ - "/.git/", - "/.yarn/", - "/dist/", - ], + testPathIgnorePatterns: ['/.git/', '/.yarn/', '/dist/'], - moduleFileExtensions: [ - "ts", - "js", - "mjs", - "cjs", - "jsx", - "ts", - "tsx", - "json", - "node", - ], + moduleFileExtensions: ['ts', 'js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'json', 'node'], - modulePathIgnorePatterns: ["/dist/"], + modulePathIgnorePatterns: ['/dist/'], - setupFiles: ["/test/setup.ts"], + setupFiles: ['/test/setup.ts'], transform: { - "\\.ts$": "babel-jest", + '\\.ts$': 'babel-jest', }, - extensionsToTreatAsEsm: [".ts"], -}; + extensionsToTreatAsEsm: ['.ts'], +} diff --git a/package.json b/package.json index bf3cfae..3443823 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-auth-library", - "version": "1.0.3", + "version": "1.0.5", "packageManager": "yarn@4.0.0-rc.39", "description": "Authentication library for the browser environment using Web Crypto API", "license": "MIT", @@ -58,20 +58,30 @@ "dist" ], "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", "exports": { ".": "./dist/index.js", - "./jwt": "./dist/core/jwt.js", "./google": { "types": "./dist/google/index.d.ts", "import": "./dist/google/index.js", "default": "./dist/google/index.js" - }, - "./package.json": "./package.json" + } + }, + "typesVersions": { + "*": { + "google": [ + "dist/google/index.d.ts" + ] + } }, "scripts": { + "dev": "tsup --watch", "lint": "eslint --report-unused-disable-directives .", + "fix": "eslint --report-unused-disable-directives --fix . && prettier --write .", "test": "node --experimental-vm-modules $(yarn bin jest)", - "build": "rm -rf ./dist && yarn tsc" + "build": "tsup" }, "dependencies": { "jose": ">= 4.12.0 < 5.0.0", @@ -94,6 +104,7 @@ "jest": "^29.4.3", "jest-environment-miniflare": "^2.12.1", "prettier": "^2.8.4", + "tsup": "^7.2.0", "typescript": "^4.9.5" }, "babel": { diff --git a/core/env.ts b/src/env.ts similarity index 74% rename from core/env.ts rename to src/env.ts index 6362e5d..18d5a28 100644 --- a/core/env.ts +++ b/src/env.ts @@ -3,4 +3,4 @@ export const canUseDefaultCache = /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - typeof (globalThis as any).caches?.default?.put === "function"; + typeof (globalThis as any).caches?.default?.put === 'function' diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 0000000..151b474 --- /dev/null +++ b/src/error.ts @@ -0,0 +1,16 @@ +/* SPDX-FileCopyrightText: 2022-present Kriasoft */ +/* SPDX-License-Identifier: MIT */ + +export class FetchError extends Error { + readonly name: string = 'FetchError' + readonly response: Response + + constructor(message: string, options: { response: Response; cause?: unknown }) { + super(message, { cause: options?.cause }) + this.response = options.response + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, Error) + } + } +} diff --git a/src/google/accessToken.test.ts b/src/google/accessToken.test.ts new file mode 100644 index 0000000..47994cd --- /dev/null +++ b/src/google/accessToken.test.ts @@ -0,0 +1,32 @@ +/* SPDX-FileCopyrightText: 2022-present Kriasoft */ +/* SPDX-License-Identifier: MIT */ + +import { decodeJwt } from 'jose' +import env from '../../test/env.js' +import { getAccessToken } from './accessToken.js' + +test('getAccessToken({ credentials, scope })', async () => { + const accessToken = await getAccessToken({ + credentials: env.GOOGLE_CLOUD_CREDENTIALS, + scope: 'https://www.googleapis.com/auth/cloud-platform', + }) + + expect(accessToken?.substring(0, 30)).toEqual(expect.stringContaining('ya29.c.')) +}) + +test('getAccessToken({ credentials, audience })', async () => { + const idToken = await getAccessToken({ + credentials: env.GOOGLE_CLOUD_CREDENTIALS, + audience: 'https://example.com', + }) + + expect(idToken?.substring(0, 30)).toEqual(expect.stringContaining('eyJhbGciOi')) + + expect(decodeJwt(idToken)).toEqual( + expect.objectContaining({ + aud: 'https://example.com', + email_verified: true, + iss: 'https://accounts.google.com', + }), + ) +}) diff --git a/src/google/accessToken.ts b/src/google/accessToken.ts new file mode 100644 index 0000000..ef6a7f5 --- /dev/null +++ b/src/google/accessToken.ts @@ -0,0 +1,197 @@ +/* SPDX-FileCopyrightText: 2022-present Kriasoft */ +/* SPDX-License-Identifier: MIT */ + +import { decodeJwt } from 'jose' +import { canUseDefaultCache } from '../env.js' +import { FetchError } from '../error.js' +import { logOnce } from '../utils.js' +import { getCredentials, type Credentials } from './credentials.js' +import { createCustomToken } from './customToken.js' + +const defaultCache = new Map() + +/** + * Fetches an access token from Google Cloud API using the provided + * service account credentials. + * + * @throws {FetchError} — If the access token could not be fetched. + */ +export async function getAccessToken(options: Options) { + if (!options?.waitUntil && canUseDefaultCache) { + logOnce('warn', 'verifyIdToken', 'Missing `waitUntil` option.') + } + + let credentials: Credentials + + // Normalize service account credentials + // using env.GOOGLE_CLOUD_CREDENTIALS as a fallback + if (options?.credentials) { + credentials = getCredentials(options.credentials) + } else { + if (!options?.env?.GOOGLE_CLOUD_CREDENTIALS) { + throw new TypeError('Missing credentials') + } + credentials = getCredentials(options.env.GOOGLE_CLOUD_CREDENTIALS) + } + + // Normalize authentication scope and audience values + const scope = Array.isArray(options.scope) ? options.scope.join(',') : options.scope + const audience = Array.isArray(options.audience) + ? options.audience.join(',') + : options.audience + + const tokenUrl = credentials.token_uri + + // Create a cache key that can be used with Cloudflare Cache API + const cacheKeyUrl = new URL(tokenUrl) + cacheKeyUrl.searchParams.set('scope', scope ?? '') + cacheKeyUrl.searchParams.set('aud', audience ?? '') + cacheKeyUrl.searchParams.set('key', credentials.private_key_id) + const cacheKey = cacheKeyUrl.toString() + + // Attempt to retrieve the token from the cache + const cache: Map = options.cache ?? defaultCache + const cacheValue = cache.get(cacheKey) + let now = Math.floor(Date.now() / 1000) + + if (cacheValue) { + if (cacheValue.created > now - 60 * 60) { + let token = await cacheValue.promise + + if (token.expires > now) { + return token.token + } else { + const nextValue = cache.get(cacheKey) + + if (nextValue && nextValue !== cacheValue) { + token = await nextValue.promise + if (token.expires > now) { + return token.token + } else { + cache.delete(cacheKey) + } + } + } + } else { + cache.delete(cacheKey) + } + } + + const promise = (async () => { + let res: Response | undefined + + // Attempt to retrieve the token from Cloudflare cache + // if the code is running in Cloudflare Workers environment + if (canUseDefaultCache) { + res = await caches.default.match(cacheKey) + } + + if (!res) { + now = Math.floor(Date.now() / 1000) + + // Request a new token from the Google Cloud API + const jwt = await createCustomToken({ + credentials, + scope: options.audience ?? options.scope, + }) + const body = new URLSearchParams() + body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer') + body.append('assertion', jwt) + res = await fetch(tokenUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body, + }) + + if (!res.ok) { + const error = await res + .json<{ error_description?: string }>() + .then((data) => data?.error_description) + .catch(() => undefined) + throw new FetchError(error ?? 'Failed to fetch an access token.', { + response: res, + }) + } + + if (canUseDefaultCache) { + let cacheRes = res.clone() + cacheRes = new Response(cacheRes.body, cacheRes) + cacheRes.headers.set('Cache-Control', `max-age=3590, public`) + cacheRes.headers.set('Last-Modified', new Date().toUTCString()) + const cachePromise = caches.default.put(cacheKey, cacheRes) + + if (options.waitUntil) { + options.waitUntil(cachePromise) + } + } + } + + const data = await res.json() + + if ('id_token' in data) { + const claims = decodeJwt(data.id_token) + return { token: data.id_token, expires: claims.exp as number } + } + + const lastModified = res.headers.get('last-modified') + const expires = lastModified + ? Math.floor(new Date(lastModified).valueOf() / 1000) + data.expires_in + : now + data.expires_in + + return { expires, token: data.access_token } + })() + + cache.set(cacheKey, { created: now, promise }) + return await promise.then((data) => data.token) +} + +// #region Types + +type Options = { + /** + * Google Cloud service account credentials. + * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys + * @default env.GOOGLE_CLOUD_PROJECT + */ + credentials: Credentials | string + /** + * Authentication scope(s). + */ + scope?: string[] | string + /** + * Recipients that the ID token should be issued for. + */ + audience?: string[] | string + env?: { + /** + * Google Cloud project ID. + */ + GOOGLE_CLOUD_PROJECT?: string + /** + * Google Cloud service account credentials. + * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys + */ + GOOGLE_CLOUD_CREDENTIALS: string + } + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + waitUntil?: (promise: Promise) => void + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + cache?: Map +} + +type TokenResponse = + | { + access_token: string + expires_in: number + token_type: string + } + | { + id_token: string + } + +type CacheValue = { + created: number + promise: Promise<{ token: string; expires: number }> +} + +// #endregion diff --git a/google/credentials.test.ts b/src/google/credentials.test.ts similarity index 60% rename from google/credentials.test.ts rename to src/google/credentials.test.ts index cc1e0b2..29e02fe 100644 --- a/google/credentials.test.ts +++ b/src/google/credentials.test.ts @@ -1,43 +1,39 @@ /* SPDX-FileCopyrightText: 2022-present Kriasoft */ /* SPDX-License-Identifier: MIT */ -import env from "../test/env.js"; -import { - getCredentials, - getPrivateKey, - importPublicKey, -} from "./credentials.js"; +import env from '../../test/env.js' +import { getCredentials, getPrivateKey, importPublicKey } from './credentials.js' -test("getPrivateKey({ credentials })", async () => { +test('getPrivateKey({ credentials })', async () => { const privateKey = await getPrivateKey({ credentials: env.GOOGLE_CLOUD_CREDENTIALS, - }); + }) expect(privateKey).toEqual( expect.objectContaining({ algorithm: expect.objectContaining({ - hash: { name: "SHA-256" }, + hash: { name: 'SHA-256' }, modulusLength: 2048, - name: "RSASSA-PKCS1-v1_5", + name: 'RSASSA-PKCS1-v1_5', }), - }) - ); -}); + }), + ) +}) -test("importPublicKey({ keyId, certificateURL })", async () => { - const credentials = getCredentials(env.GOOGLE_CLOUD_CREDENTIALS); +test('importPublicKey({ keyId, certificateURL })', async () => { + const credentials = getCredentials(env.GOOGLE_CLOUD_CREDENTIALS) const privateKey = await importPublicKey({ keyId: credentials.private_key_id, certificateURL: credentials.client_x509_cert_url, - }); + }) expect(privateKey).toEqual( expect.objectContaining({ algorithm: expect.objectContaining({ - hash: { name: "SHA-256" }, + hash: { name: 'SHA-256' }, modulusLength: 2048, - name: "RSASSA-PKCS1-v1_5", + name: 'RSASSA-PKCS1-v1_5', }), - }) - ); -}); + }), + ) +}) diff --git a/google/credentials.ts b/src/google/credentials.ts similarity index 60% rename from google/credentials.ts rename to src/google/credentials.ts index c84444b..79500f4 100644 --- a/google/credentials.ts +++ b/src/google/credentials.ts @@ -1,21 +1,21 @@ /* SPDX-FileCopyrightText: 2022-present Kriasoft */ /* SPDX-License-Identifier: MIT */ -import { importPKCS8, importX509, KeyLike } from "jose"; -import { FetchError } from "../core/error.js"; +import { importPKCS8, importX509, KeyLike } from 'jose' +import { FetchError } from '../error.js' -const inFlight = new Map>(); -const cache = new Map(); +const inFlight = new Map>() +const cache = new Map() /** * Normalizes Google Cloud Platform (GCP) service account credentials. */ export function getCredentials(credentials: Credentials | string): Credentials { - return typeof credentials === "string" || credentials instanceof String + return typeof credentials === 'string' || credentials instanceof String ? Object.freeze(JSON.parse(credentials as string)) : Object.isFrozen(credentials) ? credentials - : Object.freeze(credentials); + : Object.freeze(credentials) } /** @@ -23,8 +23,8 @@ export function getCredentials(credentials: Credentials | string): Credentials { * service account credentials. */ export function getPrivateKey(options: { credentials: Credentials | string }) { - const credentials = getCredentials(options.credentials); - return importPKCS8(credentials.private_key, "RS256"); + const credentials = getCredentials(options.credentials) + return importPKCS8(credentials.private_key, 'RS256') } /** @@ -37,55 +37,55 @@ export async function importPublicKey(options: { /** * Public key ID (kid). */ - keyId: string; + keyId: string /** * The X.509 certificate URL. * @default "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com" */ - certificateURL?: string; + certificateURL?: string /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - waitUntil?: (promise: Promise) => void; + waitUntil?: (promise: Promise) => void }) { - const keyId = options.keyId; + const keyId = options.keyId const certificateURL = options.certificateURL ?? "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"; // prettier-ignore - const cacheKey = `${certificateURL}?key=${keyId}`; - const value = cache.get(cacheKey); - const now = Date.now(); + const cacheKey = `${certificateURL}?key=${keyId}` + const value = cache.get(cacheKey) + const now = Date.now() async function fetchKey() { // Fetch the public key from Google's servers - const res = await fetch(certificateURL); + const res = await fetch(certificateURL) if (!res.ok) { const error = await res .json<{ error: { message: string } }>() .then((data) => data.error.message) - .catch(() => undefined); - throw new FetchError(error ?? "Failed to fetch the public key", { + .catch(() => undefined) + throw new FetchError(error ?? 'Failed to fetch the public key', { response: res, - }); + }) } - const data = await res.json>(); - const x509 = data[keyId]; + const data = await res.json>() + const x509 = data[keyId] if (!x509) { throw new FetchError(`Public key "${keyId}" not found.`, { response: res, - }); + }) } - const key = await importX509(x509, "RS256"); + const key = await importX509(x509, 'RS256') // Resolve the expiration time of the key const maxAge = res.headers.get("cache-control")?.match(/max-age=(\d+)/)?.[1]; // prettier-ignore - const expires = Date.now() + Number(maxAge ?? "3600") * 1000; + const expires = Date.now() + Number(maxAge ?? '3600') * 1000 // Update the local cache - cache.set(cacheKey, { key, expires }); - inFlight.delete(keyId); + cache.set(cacheKey, { key, expires }) + inFlight.delete(keyId) - return key; + return key } // Attempt to read the key from the local cache @@ -93,28 +93,28 @@ export async function importPublicKey(options: { if (value.expires > now + 10_000) { // If the key is about to expire, start a new request in the background if (value.expires - now < 600_000) { - const promise = fetchKey(); - inFlight.set(cacheKey, promise); + const promise = fetchKey() + inFlight.set(cacheKey, promise) if (options.waitUntil) { - options.waitUntil(promise); + options.waitUntil(promise) } } - return value.key; + return value.key } else { - cache.delete(cacheKey); + cache.delete(cacheKey) } } // Check if there is an in-flight request for the same key ID - let promise = inFlight.get(cacheKey); + let promise = inFlight.get(cacheKey) // If not, start a new request if (!promise) { - promise = fetchKey(); - inFlight.set(cacheKey, promise); + promise = fetchKey() + inFlight.set(cacheKey, promise) } - return await promise; + return await promise } /** @@ -123,14 +123,14 @@ export async function importPublicKey(options: { * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys */ export type Credentials = { - type: string; - project_id: string; - private_key_id: string; - private_key: string; - client_id: string; - client_email: string; - auth_uri: string; - token_uri: string; - auth_provider_x509_cert_url: string; - client_x509_cert_url: string; -}; + type: string + project_id: string + private_key_id: string + private_key: string + client_id: string + client_email: string + auth_uri: string + token_uri: string + auth_provider_x509_cert_url: string + client_x509_cert_url: string +} diff --git a/src/google/customToken.test.ts b/src/google/customToken.test.ts new file mode 100644 index 0000000..85381e3 --- /dev/null +++ b/src/google/customToken.test.ts @@ -0,0 +1,70 @@ +/* SPDX-FileCopyrightText: 2022-present Kriasoft */ +/* SPDX-License-Identifier: MIT */ + +import { decodeJwt } from 'jose' +import env from '../../test/env.js' +import { createCustomToken } from './customToken.js' + +test('createCustomToken({ credentials, scope })', async () => { + const customToken = await createCustomToken({ + credentials: env.GOOGLE_CLOUD_CREDENTIALS, + scope: 'https://www.example.com', + }) + + expect(customToken?.substring(0, 30)).toEqual(expect.stringContaining('eyJhbGciOi')) + + expect(decodeJwt(customToken)).toEqual( + expect.objectContaining({ + iss: expect.stringMatching(/\.iam\.gserviceaccount\.com$/), + aud: 'https://oauth2.googleapis.com/token', + scope: 'https://www.example.com', + iat: expect.any(Number), + exp: expect.any(Number), + }), + ) +}) + +test('createCustomToken({ credentials, scope: scopes })', async () => { + const customToken = await createCustomToken({ + credentials: env.GOOGLE_CLOUD_CREDENTIALS, + scope: ['https://www.example.com', 'https://beta.example.com'], + }) + + expect(customToken?.substring(0, 30)).toEqual(expect.stringContaining('eyJhbGciOi')) + + expect(decodeJwt(customToken)).toEqual( + expect.objectContaining({ + iss: expect.stringMatching(/\.iam\.gserviceaccount\.com$/), + aud: 'https://oauth2.googleapis.com/token', + scope: 'https://www.example.com https://beta.example.com', + iat: expect.any(Number), + exp: expect.any(Number), + }), + ) +}) + +test('createCustomToken({ env, scope })', async () => { + const customToken = await createCustomToken({ + scope: 'https://www.googleapis.com/auth/cloud-platform', + env: { GOOGLE_CLOUD_CREDENTIALS: env.GOOGLE_CLOUD_CREDENTIALS }, + }) + + expect(customToken?.substring(0, 30)).toEqual(expect.stringContaining('eyJhbGciOi')) + + expect(decodeJwt(customToken)).toEqual( + expect.objectContaining({ + iss: expect.stringMatching(/\.iam\.gserviceaccount\.com$/), + aud: 'https://oauth2.googleapis.com/token', + scope: 'https://www.googleapis.com/auth/cloud-platform', + iat: expect.any(Number), + exp: expect.any(Number), + }), + ) +}) + +test('createCustomToken({ env, scope })', async () => { + const promise = createCustomToken({ + scope: 'https://www.googleapis.com/auth/cloud-platform', + }) + expect(promise).rejects.toThrow(new TypeError('Missing credentials')) +}) diff --git a/google/customToken.ts b/src/google/customToken.ts similarity index 72% rename from google/customToken.ts rename to src/google/customToken.ts index a499b88..ae3e1b9 100644 --- a/google/customToken.ts +++ b/src/google/customToken.ts @@ -1,12 +1,8 @@ /* SPDX-FileCopyrightText: 2022-present Kriasoft */ /* SPDX-License-Identifier: MIT */ -import { SignJWT } from "jose"; -import { - getCredentials, - getPrivateKey, - type Credentials, -} from "./credentials.js"; +import { SignJWT } from 'jose' +import { getCredentials, getPrivateKey, type Credentials } from './credentials.js' /** * Generates a custom authentication token (JWT) @@ -30,24 +26,24 @@ export async function createCustomToken(options: { * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys * @default env.GOOGLE_CLOUD_PROJECT */ - credentials?: Credentials | string; + credentials?: Credentials | string /** * Authentication scope. * @example "https://www.googleapis.com/auth/cloud-platform" */ - scope?: string | string[]; + scope?: string | string[] /** * The principal that is the subject of the JWT. */ - subject?: string; + subject?: string /** * The recipient(s) that the JWT is intended for. */ - audience?: string | string[]; + audience?: string | string[] /** * Any other JWT clams. */ - [propName: string]: unknown; + [propName: string]: unknown /** * Alternatively, you can pass credentials via the environment variable. */ @@ -56,35 +52,35 @@ export async function createCustomToken(options: { * Google Cloud service account credentials. * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys */ - GOOGLE_CLOUD_CREDENTIALS: string; - }; + GOOGLE_CLOUD_CREDENTIALS: string + } }) { /* eslint-disable-next-line prefer-const */ - let { credentials, scope, subject, audience, env, ...payload } = options; + let { credentials, scope, subject, audience, env, ...payload } = options // Normalize credentials using env.GOOGLE_CLOUD_CREDENTIALS as a fallback if (credentials) { - credentials = getCredentials(credentials); + credentials = getCredentials(credentials) } else { if (!env?.GOOGLE_CLOUD_CREDENTIALS) { - throw new TypeError("Missing credentials"); + throw new TypeError('Missing credentials') } - credentials = getCredentials(env.GOOGLE_CLOUD_CREDENTIALS); + credentials = getCredentials(env.GOOGLE_CLOUD_CREDENTIALS) } // Normalize authentication scope (needs to be a string) - scope = Array.isArray(scope) ? scope.join(" ") : scope; + scope = Array.isArray(scope) ? scope.join(' ') : scope // Generate and sign a custom JWT token - const privateKey = await getPrivateKey({ credentials }); + const privateKey = await getPrivateKey({ credentials }) const customToken = await new SignJWT({ scope, ...payload }) .setIssuer(credentials.client_email) .setAudience(audience ?? credentials.token_uri) .setSubject(subject ?? credentials.client_email) - .setProtectedHeader({ alg: "RS256" }) + .setProtectedHeader({ alg: 'RS256' }) .setIssuedAt() - .setExpirationTime("1h") - .sign(privateKey); + .setExpirationTime('1h') + .sign(privateKey) - return customToken; + return customToken } diff --git a/google/idToken.test.ts b/src/google/idToken.test.ts similarity index 64% rename from google/idToken.test.ts rename to src/google/idToken.test.ts index 33409b3..4804974 100644 --- a/google/idToken.test.ts +++ b/src/google/idToken.test.ts @@ -1,57 +1,57 @@ /* SPDX-FileCopyrightText: 2022-present Kriasoft */ /* SPDX-License-Identifier: MIT */ -import { decodeJwt } from "jose"; -import env from "../test/env.js"; -import { getIdToken, verifyIdToken } from "./idToken.js"; +import { decodeJwt } from 'jose' +import env from '../../test/env.js' +import { getIdToken, verifyIdToken } from './idToken.js' -test("getIdToken({ uid, apiKey, projectId, credentials })", async () => { +test('getIdToken({ uid, apiKey, projectId, credentials })', async () => { const token = await getIdToken({ - uid: "temp", - claims: { foo: "bar" }, + uid: 'temp', + claims: { foo: 'bar' }, apiKey: env.FIREBASE_API_KEY, credentials: env.GOOGLE_CLOUD_CREDENTIALS, - }); + }) expect(token).toEqual( expect.objectContaining({ - kind: "identitytoolkit#VerifyCustomTokenResponse", + kind: 'identitytoolkit#VerifyCustomTokenResponse', idToken: expect.stringMatching(/^eyJhbGciOiJSUzI1NiIs/), refreshToken: expect.any(String), - expiresIn: "3600", + expiresIn: '3600', isNewUser: expect.any(Boolean), - }) - ); + }), + ) expect(decodeJwt(token.idToken)).toEqual( expect.objectContaining({ - sub: "temp", - user_id: "temp", + sub: 'temp', + user_id: 'temp', aud: env.GOOGLE_CLOUD_PROJECT, iss: `https://securetoken.google.com/${env.GOOGLE_CLOUD_PROJECT}`, iat: expect.any(Number), exp: expect.any(Number), auth_time: expect.any(Number), - }) - ); -}); + }), + ) +}) -test("verifyIdToken({ idToken })", async () => { +test('verifyIdToken({ idToken })', async () => { const { idToken } = await getIdToken({ - uid: "temp", + uid: 'temp', apiKey: env.FIREBASE_API_KEY, credentials: env.GOOGLE_CLOUD_CREDENTIALS, - }); - const token = await verifyIdToken({ idToken, env }); + }) + const token = await verifyIdToken({ idToken, env }) expect(token).toEqual( expect.objectContaining({ aud: env.GOOGLE_CLOUD_PROJECT, iss: `https://securetoken.google.com/${env.GOOGLE_CLOUD_PROJECT}`, - sub: "temp", - user_id: "temp", + sub: 'temp', + user_id: 'temp', iat: expect.any(Number), exp: expect.any(Number), - }) - ); -}); + }), + ) +}) diff --git a/google/idToken.ts b/src/google/idToken.ts similarity index 64% rename from google/idToken.ts rename to src/google/idToken.ts index c2e24f6..2371cd6 100644 --- a/google/idToken.ts +++ b/src/google/idToken.ts @@ -1,12 +1,12 @@ /* SPDX-FileCopyrightText: 2022-present Kriasoft */ /* SPDX-License-Identifier: MIT */ -import { decodeProtectedHeader, errors, jwtVerify } from "jose"; -import { canUseDefaultCache } from "../core/env.js"; -import { FetchError } from "../core/error.js"; -import { logOnce } from "../core/utils.js"; -import { Credentials, getCredentials, importPublicKey } from "./credentials.js"; -import { createCustomToken } from "./customToken.js"; +import { decodeProtectedHeader, errors, jwtVerify } from 'jose' +import { canUseDefaultCache } from '../env.js' +import { FetchError } from '../error.js' +import { logOnce } from '../utils.js' +import { Credentials, getCredentials, importPublicKey } from './credentials.js' +import { createCustomToken } from './customToken.js' /** * Creates a User ID token using Google Cloud service account credentials. @@ -15,28 +15,28 @@ export async function getIdToken(options: { /** * User ID. */ - uid: string; + uid: string /** * Additional user claims. */ - claims?: Record; + claims?: Record /** * Google Cloud API key. * @see https://console.cloud.google.com/apis/credentials * @default env.FIREBASE_API_KEY */ - apiKey?: string; + apiKey?: string /** * Google Cloud project ID. * @default env.GOOGLE_CLOUD_PROJECT; */ - projectId?: string; + projectId?: string /** * Google Cloud service account credentials. * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys * @default env.GOOGLE_CLOUD_PROJECT */ - credentials?: Credentials | string; + credentials?: Credentials | string /** * Alternatively, you can pass credentials via the environment variable. */ @@ -45,91 +45,91 @@ export async function getIdToken(options: { * Google Cloud API key. * @see https://console.cloud.google.com/apis/credentials */ - FIREBASE_API_KEY: string; + FIREBASE_API_KEY: string /** * Google Cloud project ID. */ - GOOGLE_CLOUD_PROJECT: string; + GOOGLE_CLOUD_PROJECT: string /** * Google Cloud service account credentials. * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys */ - GOOGLE_CLOUD_CREDENTIALS: string; - }; + GOOGLE_CLOUD_CREDENTIALS: string + } }) { - const uid = options?.uid; + const uid = options?.uid if (!uid) { - throw new TypeError("Missing uid"); + throw new TypeError('Missing uid') } - let apiKey = options?.apiKey; + let apiKey = options?.apiKey if (!apiKey) { if (options?.env?.FIREBASE_API_KEY) { - apiKey = options.env.FIREBASE_API_KEY; + apiKey = options.env.FIREBASE_API_KEY } else { - throw new TypeError("Missing apiKey"); + throw new TypeError('Missing apiKey') } } - let credentials = options?.credentials; + let credentials = options?.credentials if (credentials) { - credentials = getCredentials(credentials); + credentials = getCredentials(credentials) } else { if (options?.env?.GOOGLE_CLOUD_CREDENTIALS) { - credentials = getCredentials(options.env.GOOGLE_CLOUD_CREDENTIALS); + credentials = getCredentials(options.env.GOOGLE_CLOUD_CREDENTIALS) } else { - throw new TypeError("Missing credentials"); + throw new TypeError('Missing credentials') } } - let projectId = options?.projectId; + let projectId = options?.projectId if (!projectId && options?.env?.GOOGLE_CLOUD_PROJECT) { - projectId = options.env.GOOGLE_CLOUD_PROJECT; + projectId = options.env.GOOGLE_CLOUD_PROJECT } if (!projectId) { - projectId = credentials.project_id; + projectId = credentials.project_id } if (!projectId) { - throw new TypeError("Missing projectId"); + throw new TypeError('Missing projectId') } const customToken = await createCustomToken({ ...options.claims, credentials, audience: - "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit", + 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit', uid: options.uid, - }); + }) const url = new URL("https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken"); // prettier-ignore - url.searchParams.set("key", apiKey); + url.searchParams.set('key', apiKey) const res = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: customToken, returnSecureToken: true, }), - }); + }) if (!res.ok) { const message = await res .json<{ error: { message: string } }>() .then((body) => body?.error?.message) - .catch(() => undefined); - throw new FetchError(message ?? "Failed to verify custom token", { + .catch(() => undefined) + throw new FetchError(message ?? 'Failed to verify custom token', { response: res, - }); + }) } - return await res.json(); + return await res.json() } /** @@ -160,12 +160,12 @@ export async function verifyIdToken(options: { /** * The ID token to verify. */ - idToken: string; + idToken: string /** * Google Cloud project ID. Set to `null` to disable the check. * @default env.GOOGLE_CLOUD_PROJECT */ - projectId?: string | null; + projectId?: string | null /** * Alternatively, you can provide the following environment variables: */ @@ -173,144 +173,141 @@ export async function verifyIdToken(options: { /** * Google Cloud project ID. */ - GOOGLE_CLOUD_PROJECT?: string; + GOOGLE_CLOUD_PROJECT?: string /** * Google Cloud service account credentials. * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys */ - GOOGLE_CLOUD_CREDENTIALS?: string; - }; + GOOGLE_CLOUD_CREDENTIALS?: string + } /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - waitUntil?: (promise: Promise) => void; + waitUntil?: (promise: Promise) => void }): Promise { if (!options?.idToken) { - throw new TypeError(`Missing "idToken"`); + throw new TypeError(`Missing "idToken"`) } - let projectId = options?.projectId; + let projectId = options?.projectId if (projectId === undefined) { - projectId = options?.env?.GOOGLE_CLOUD_PROJECT; + projectId = options?.env?.GOOGLE_CLOUD_PROJECT } if (projectId === undefined && options?.env?.GOOGLE_CLOUD_CREDENTIALS) { - const credentials = getCredentials(options.env.GOOGLE_CLOUD_CREDENTIALS); - projectId = credentials?.project_id; + const credentials = getCredentials(options.env.GOOGLE_CLOUD_CREDENTIALS) + projectId = credentials?.project_id } if (projectId === undefined) { - throw new TypeError(`Missing "projectId"`); + throw new TypeError(`Missing "projectId"`) } if (!options.waitUntil && canUseDefaultCache) { - logOnce("warn", "verifyIdToken", "Missing `waitUntil` option."); + logOnce('warn', 'verifyIdToken', 'Missing `waitUntil` option.') } // Import the public key from the Google Cloud project - const header = decodeProtectedHeader(options.idToken); - const now = Math.floor(Date.now() / 1000); + const header = decodeProtectedHeader(options.idToken) + const now = Math.floor(Date.now() / 1000) const key = await importPublicKey({ keyId: header.kid as string, certificateURL: "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com", // prettier-ignore waitUntil: options.waitUntil, - }); + }) const { payload } = await jwtVerify(options.idToken, key, { audience: projectId == null ? undefined : projectId, - issuer: - projectId == null - ? undefined - : `https://securetoken.google.com/${projectId}`, - maxTokenAge: "1h", - }); + issuer: projectId == null ? undefined : `https://securetoken.google.com/${projectId}`, + maxTokenAge: '1h', + }) if (!payload.sub) { - throw new errors.JWTClaimValidationFailed(`Missing "sub" claim`, "sub"); + throw new errors.JWTClaimValidationFailed(`Missing "sub" claim`, 'sub') } - if (typeof payload.auth_time === "number" && payload.auth_time > now) { + if (typeof payload.auth_time === 'number' && payload.auth_time > now) { throw new errors.JWTClaimValidationFailed( `Unexpected "auth_time" claim value`, - "auth_time" - ); + 'auth_time', + ) } - return payload as UserToken; + return payload as UserToken } type VerifyCustomTokenResponse = { - kind: "identitytoolkit#VerifyCustomTokenResponse"; - idToken: string; - refreshToken: string; - expiresIn: string; - isNewUser: boolean; -}; + kind: 'identitytoolkit#VerifyCustomTokenResponse' + idToken: string + refreshToken: string + expiresIn: string + isNewUser: boolean +} export interface UserToken { /** * Always set to https://securetoken.google.com/GOOGLE_CLOUD_PROJECT */ - iss: string; + iss: string /** * Always set to GOOGLE_CLOUD_PROJECT */ - aud: string; + aud: string /** * The user's unique ID */ - sub: string; + sub: string /** * The token issue time, in seconds since epoch */ - iat: number; + iat: number /** * The token expiry time, normally 'iat' + 3600 */ - exp: number; + exp: number /** * The user's unique ID. Must be equal to 'sub' */ - user_id: string; + user_id: string /** * The time the user authenticated, normally 'iat' */ - auth_time: number; + auth_time: number /** * The sign in provider, only set when the provider is 'anonymous' */ - provider_id?: "anonymous"; + provider_id?: 'anonymous' /** * The user's primary email */ - email?: string; + email?: string /** * The user's email verification status */ - email_verified?: boolean; + email_verified?: boolean /** * The user's primary phone number */ - phone_number?: string; + phone_number?: string /** * The user's display name */ - name?: string; + name?: string /** * The user's profile photo URL */ - picture?: string; + picture?: string /** * Information on all identities linked to this user @@ -319,35 +316,35 @@ export interface UserToken { /** * The primary sign-in provider */ - sign_in_provider: SignInProvider; + sign_in_provider: SignInProvider /** * A map of providers to the user's list of unique identifiers from * each provider */ - identities?: { [provider in SignInProvider]?: string[] }; - }; + identities?: { [provider in SignInProvider]?: string[] } + } /** * Custom claims set by the developer */ - [claim: string]: unknown; + [claim: string]: unknown /** * @deprecated use `sub` instead */ - uid?: never; + uid?: never } export type SignInProvider = - | "custom" - | "email" - | "password" - | "phone" - | "anonymous" - | "google.com" - | "facebook.com" - | "github.com" - | "twitter.com" - | "microsoft.com" - | "apple.com"; + | 'custom' + | 'email' + | 'password' + | 'phone' + | 'anonymous' + | 'google.com' + | 'facebook.com' + | 'github.com' + | 'twitter.com' + | 'microsoft.com' + | 'apple.com' diff --git a/src/google/index.ts b/src/google/index.ts new file mode 100644 index 0000000..99cba8f --- /dev/null +++ b/src/google/index.ts @@ -0,0 +1,7 @@ +/* SPDX-FileCopyrightText: 2022-present Kriasoft */ +/* SPDX-License-Identifier: MIT */ + +export * from './accessToken.js' +export * from './credentials.js' +export * from './customToken.js' +export * from './idToken.js' diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..8099dec --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export * from './jwt.js' diff --git a/core/jwt.test.ts b/src/jwt.test.ts similarity index 85% rename from core/jwt.test.ts rename to src/jwt.test.ts index 7bed28a..62ae4d9 100644 --- a/core/jwt.test.ts +++ b/src/jwt.test.ts @@ -1,11 +1,11 @@ /* SPDX-FileCopyrightText: 2022-present Kriasoft */ /* SPDX-License-Identifier: MIT */ -import { jwt } from "../index.js"; +import * as jwt from './jwt.js' -test("jwt.decode(token)", () => { +test('jwt.decode(token)', () => { const token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJleHAiOjEzOTMyODY4OTMsImlhdCI6MTM5MzI2ODg5M30.4-iaDojEVl0pJQMjrbM1EzUIfAZgsbK_kgnVyVxFSVo"; // prettier-ignore - const result = jwt.decode(token); + const result = jwt.decode(token) expect(result).toMatchInlineSnapshot(` { @@ -21,12 +21,12 @@ test("jwt.decode(token)", () => { }, "signature": "4-iaDojEVl0pJQMjrbM1EzUIfAZgsbK_kgnVyVxFSVo", } - `); -}); + `) +}) -test("jwt.decode(unicodeToken)", () => { +test('jwt.decode(unicodeToken)', () => { const unicodeToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9zw6kiLCJpYXQiOjE0MjU2NDQ5NjZ9.1CfFtdGUPs6q8kT3OGQSVlhEMdbuX0HfNSqum0023a0"; // prettier-ignore - const result = jwt.decode(unicodeToken); + const result = jwt.decode(unicodeToken) expect(result).toMatchInlineSnapshot(` { @@ -41,12 +41,12 @@ test("jwt.decode(unicodeToken)", () => { }, "signature": "1CfFtdGUPs6q8kT3OGQSVlhEMdbuX0HfNSqum0023a0", } - `); -}); + `) +}) -test("jwt.decode(binaryToken)", () => { +test('jwt.decode(binaryToken)', () => { const binaryToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9z6SIsImlhdCI6MTQyNTY0NDk2Nn0.cpnplCBxiw7Xqz5thkqs4Mo_dymvztnI0CI4BN0d1t8"; // prettier-ignore - const result = jwt.decode(binaryToken); + const result = jwt.decode(binaryToken) expect(result).toMatchInlineSnapshot(` { @@ -61,5 +61,5 @@ test("jwt.decode(binaryToken)", () => { }, "signature": "cpnplCBxiw7Xqz5thkqs4Mo_dymvztnI0CI4BN0d1t8", } - `); -}); + `) +}) diff --git a/core/jwt.ts b/src/jwt.ts similarity index 60% rename from core/jwt.ts rename to src/jwt.ts index 36efa44..396aab4 100644 --- a/core/jwt.ts +++ b/src/jwt.ts @@ -1,40 +1,36 @@ /* SPDX-FileCopyrightText: 2022-present Kriasoft */ /* SPDX-License-Identifier: MIT */ -import { base64url } from "rfc4648"; +import { base64url } from 'rfc4648' /** * Converts the given JSON Web Token string into a `Jwt` object. */ function decode(token: string): Jwt { - const segments = token.split("."); - const dec = new TextDecoder(); + const segments = token.split('.') + const dec = new TextDecoder() if (segments.length !== 3) { - throw new Error(); + throw new Error() } return { - header: JSON.parse( - dec.decode(base64url.parse(segments[0], { loose: true })) - ), + header: JSON.parse(dec.decode(base64url.parse(segments[0], { loose: true }))), - payload: JSON.parse( - dec.decode(base64url.parse(segments[1], { loose: true })) - ), + payload: JSON.parse(dec.decode(base64url.parse(segments[1], { loose: true }))), data: `${segments[0]}.${segments[1]}`, signature: segments[2], - }; + } } async function verify( token: Jwt | string, - options: VerifyOptions + options: VerifyOptions, ): Promise { - const enc = new TextEncoder(); - const jwt = typeof token === "string" ? decode(token) : token; - const aud = (jwt.payload as { aud?: string }).aud; + const enc = new TextEncoder() + const jwt = typeof token === 'string' ? decode(token) : token + const aud = (jwt.payload as { aud?: string }).aud if ( options.audience && @@ -42,17 +38,17 @@ async function verify( (Array.isArray(options.audience) && !options.audience.includes(aud)) || options.audience !== aud) ) { - return; + return } const verified = await crypto.subtle.verify( options.key.algorithm, options.key, base64url.parse(jwt.signature, { loose: true }), - enc.encode(jwt.data) - ); + enc.encode(jwt.data), + ) - return verified ? jwt.payload : undefined; + return verified ? jwt.payload : undefined } /* ------------------------------------------------------------------------------- * @@ -64,19 +60,19 @@ async function verify( */ interface JwtHeader { /** Token type */ - typ?: string; + typ?: string /** Content type*/ - cty?: string; + cty?: string /** Message authentication code algorithm */ - alg?: string; + alg?: string /** Key ID */ - kid?: string; + kid?: string /** x.509 Certificate Chain */ - x5c?: string; + x5c?: string /** x.509 Certificate Chain URL */ - x5u?: string; + x5u?: string /** Critical */ - crit?: string; + crit?: string } /** @@ -84,43 +80,36 @@ interface JwtHeader { */ interface JwtPayload { /** Issuer */ - iss?: string; + iss?: string /** Subject */ - sub?: string; + sub?: string /** Audience */ - aud?: string; + aud?: string /** Authorized party */ - azp?: string; + azp?: string /** Expiration time */ - exp?: number; + exp?: number /** Not before */ - nbf?: number; + nbf?: number /** Issued at */ - iat?: number; + iat?: number /** JWT ID */ - jti?: string; + jti?: string } /** * JSON Web Token (JWT) */ type Jwt = { - header: H; - payload: T; - data: string; - signature: string; -}; + header: H + payload: T + data: string + signature: string +} type VerifyOptions = { - key: CryptoKey; - audience?: string[] | string; -}; - -export { - decode, - verify, - type Jwt, - type JwtHeader, - type JwtPayload, - type VerifyOptions, -}; + key: CryptoKey + audience?: string[] | string +} + +export { decode, verify, type Jwt, type JwtHeader, type JwtPayload, type VerifyOptions } diff --git a/core/utils.ts b/src/utils.ts similarity index 59% rename from core/utils.ts rename to src/utils.ts index f43c212..fd570e8 100644 --- a/core/utils.ts +++ b/src/utils.ts @@ -1,12 +1,12 @@ /* SPDX-FileCopyrightText: 2022-present Kriasoft */ /* SPDX-License-Identifier: MIT */ -const logOnceKeys = new Set(); -type Severity = "log" | "warn" | "error"; +const logOnceKeys = new Set() +type Severity = 'log' | 'warn' | 'error' export function logOnce(severity: Severity, key: string, message: string) { if (!logOnceKeys.has(key)) { - logOnceKeys.add(key); - console[severity](message); + logOnceKeys.add(key) + console[severity](message) } } diff --git a/test/env.ts b/test/env.ts index 77b761a..6df30fe 100644 --- a/test/env.ts +++ b/test/env.ts @@ -1,10 +1,10 @@ /* SPDX-FileCopyrightText: 2022-present Kriasoft */ /* SPDX-License-Identifier: MIT */ -import { cleanEnv, str } from "envalid"; +import { cleanEnv, str } from 'envalid' export default cleanEnv(process.env, { GOOGLE_CLOUD_PROJECT: str(), GOOGLE_CLOUD_CREDENTIALS: str(), FIREBASE_API_KEY: str(), -}); +}) diff --git a/test/setup.ts b/test/setup.ts index 522911e..8407fc9 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,7 +1,7 @@ /* SPDX-FileCopyrightText: 2022-present Kriasoft */ /* SPDX-License-Identifier: MIT */ -import dotenv from "dotenv"; +import dotenv from 'dotenv' -dotenv.config({ path: "./test/test.override.env" }); -dotenv.config({ path: "./test/test.env" }); +dotenv.config({ path: './test/test.override.env' }) +dotenv.config({ path: './test/test.env' }) diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..ccb2f47 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + format: ['esm', 'cjs'], + entry: ['src/index.ts', 'src/google/index.ts'], + dts: true, + clean: true, +}) diff --git a/yarn.lock b/yarn.lock index 192716d..cd9ad82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1374,6 +1374,160 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/android-arm64@npm:0.18.20" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/android-arm@npm:0.18.20" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/android-x64@npm:0.18.20" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/darwin-arm64@npm:0.18.20" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/darwin-x64@npm:0.18.20" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/freebsd-arm64@npm:0.18.20" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/freebsd-x64@npm:0.18.20" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-arm64@npm:0.18.20" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-arm@npm:0.18.20" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-ia32@npm:0.18.20" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-loong64@npm:0.18.20" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-mips64el@npm:0.18.20" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-ppc64@npm:0.18.20" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-riscv64@npm:0.18.20" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-s390x@npm:0.18.20" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-x64@npm:0.18.20" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/netbsd-x64@npm:0.18.20" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/openbsd-x64@npm:0.18.20" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/sunos-x64@npm:0.18.20" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/win32-arm64@npm:0.18.20" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/win32-ia32@npm:0.18.20" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/win32-x64@npm:0.18.20" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^2.0.0": version: 2.0.0 resolution: "@eslint/eslintrc@npm:2.0.0" @@ -2384,6 +2538,13 @@ __metadata: languageName: node linkType: hard +"any-promise@npm:^1.0.0": + version: 1.3.0 + resolution: "any-promise@npm:1.3.0" + checksum: 5768f5c5c10b5152048e2e4e44ba3509a9f3d0dfd8e73de34099adb6f05068966fa34feda164131a901fb37977d996f84a76a7ef120eff2f93725646937b4751 + languageName: node + linkType: hard + "anymatch@npm:^3.0.3, anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" @@ -2634,6 +2795,17 @@ __metadata: languageName: node linkType: hard +"bundle-require@npm:^4.0.0": + version: 4.0.1 + resolution: "bundle-require@npm:4.0.1" + dependencies: + load-tsconfig: "npm:^0.2.3" + peerDependencies: + esbuild: ">=0.17" + checksum: a0c892e3ba4214d2506acbdbe6029c0b840cdb2a80d19614f34f9544c07acd2e90f38f88bba99f1324c6c4492523e6aa0413f3a69fc890972267fe6a7d3fed67 + languageName: node + linkType: hard + "busboy@npm:^1.6.0": version: 1.6.0 resolution: "busboy@npm:1.6.0" @@ -2643,6 +2815,13 @@ __metadata: languageName: node linkType: hard +"cac@npm:^6.7.12": + version: 6.7.14 + resolution: "cac@npm:6.7.14" + checksum: 2cc8918bf80255abb06825da69275294c4cacf4a282c8a6297cce401f785e225c73c1aa2f32d615d9a77ba9ee9ced6cf3d3e606ddf594ab8d95f6b3525807c9e + languageName: node + linkType: hard + "cacache@npm:^16.1.0": version: 16.1.3 resolution: "cacache@npm:16.1.3" @@ -2725,7 +2904,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.4.0": +"chokidar@npm:^3.4.0, chokidar@npm:^3.5.1": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -2838,7 +3017,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^4.0.1": +"commander@npm:^4.0.0, commander@npm:^4.0.1": version: 4.1.1 resolution: "commander@npm:4.1.1" checksum: 3be44d4e8e108ce5056885db1ee90cf34afe5b1c965829c23b3a47890d27980e101889fe7355accd6ec22cad862abc9f609da6de0c4c061e19d04d098611baf4 @@ -2893,7 +3072,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -3048,6 +3227,83 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.18.2": + version: 0.18.20 + resolution: "esbuild@npm:0.18.20" + dependencies: + "@esbuild/android-arm": "npm:0.18.20" + "@esbuild/android-arm64": "npm:0.18.20" + "@esbuild/android-x64": "npm:0.18.20" + "@esbuild/darwin-arm64": "npm:0.18.20" + "@esbuild/darwin-x64": "npm:0.18.20" + "@esbuild/freebsd-arm64": "npm:0.18.20" + "@esbuild/freebsd-x64": "npm:0.18.20" + "@esbuild/linux-arm": "npm:0.18.20" + "@esbuild/linux-arm64": "npm:0.18.20" + "@esbuild/linux-ia32": "npm:0.18.20" + "@esbuild/linux-loong64": "npm:0.18.20" + "@esbuild/linux-mips64el": "npm:0.18.20" + "@esbuild/linux-ppc64": "npm:0.18.20" + "@esbuild/linux-riscv64": "npm:0.18.20" + "@esbuild/linux-s390x": "npm:0.18.20" + "@esbuild/linux-x64": "npm:0.18.20" + "@esbuild/netbsd-x64": "npm:0.18.20" + "@esbuild/openbsd-x64": "npm:0.18.20" + "@esbuild/sunos-x64": "npm:0.18.20" + "@esbuild/win32-arm64": "npm:0.18.20" + "@esbuild/win32-ia32": "npm:0.18.20" + "@esbuild/win32-x64": "npm:0.18.20" + dependenciesMeta: + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: e8ff43647209dd6f29bbdbfbeed578f8a20763c2e5714e53d01afcf3a12bc097b03a8eb14f5f4e26965b8336e6f349978e3fa7778abc5bd2dac45544a36ca739 + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -3503,6 +3759,20 @@ __metadata: languageName: node linkType: hard +"glob@npm:7.1.6": + version: 7.1.6 + resolution: "glob@npm:7.1.6" + dependencies: + fs.realpath: "npm:^1.0.0" + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:^3.0.4" + once: "npm:^1.3.0" + path-is-absolute: "npm:^1.0.0" + checksum: d50636c269f66c01b688468f60eea9fd8fe98f8c1dc9837fd7767229b47274eeb3c18a1b5c314ce53550d05326d33d9ec531194d8b908fb312cf658664c8cc29 + languageName: node + linkType: hard + "glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.2.0": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -3546,7 +3816,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.1.0": +"globby@npm:^11.0.3, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -4369,6 +4639,13 @@ __metadata: languageName: node linkType: hard +"joycon@npm:^3.0.1": + version: 3.1.1 + resolution: "joycon@npm:3.1.1" + checksum: a51b680763b484e3bc516a33e959db12fb61fa8f58130e060151e8412607256b3647d97d5a16e66bd990d8a4a319a36b185af3f119340c4362c06faf38900d08 + languageName: node + linkType: hard + "js-sdsl@npm:^4.1.4": version: 4.3.0 resolution: "js-sdsl@npm:4.3.0" @@ -4485,6 +4762,13 @@ __metadata: languageName: node linkType: hard +"lilconfig@npm:^2.0.5": + version: 2.1.0 + resolution: "lilconfig@npm:2.1.0" + checksum: 1c7c643ccda7eb00b0d904912c1d7ea9cc36fe2e4e7e752b940daa9ba9550049c5ec1375f835cda58b9a917f6b0fbcae63617c1f63c139c1a20217dae4e58f39 + languageName: node + linkType: hard + "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -4492,6 +4776,13 @@ __metadata: languageName: node linkType: hard +"load-tsconfig@npm:^0.2.3": + version: 0.2.5 + resolution: "load-tsconfig@npm:0.2.5" + checksum: 27431b235c964fcf71e037171103b079e323ff19905f2aabf1cb47c028ec2dd3ada6cc203cef79d368cfaf0c1505e04a83030fe63257e114eed826b3b7c78dfa + languageName: node + linkType: hard + "locate-path@npm:^5.0.0": version: 5.0.0 resolution: "locate-path@npm:5.0.0" @@ -4524,6 +4815,13 @@ __metadata: languageName: node linkType: hard +"lodash.sortby@npm:^4.7.0": + version: 4.7.0 + resolution: "lodash.sortby@npm:4.7.0" + checksum: 533eff6eecb504d3fdfe33e994bf89dd1ed377172b6b82b2690b60e0edd80befa5ad1a4089c2714c564c6f239406d40caac328e3daa16a33fa359263ec501a4e + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -4757,6 +5055,17 @@ __metadata: languageName: node linkType: hard +"mz@npm:^2.7.0": + version: 2.7.0 + resolution: "mz@npm:2.7.0" + dependencies: + any-promise: "npm:^1.0.0" + object-assign: "npm:^4.0.1" + thenify-all: "npm:^1.0.0" + checksum: 94100397dc4e8b8451c743b025bbd9a8fa8bb7c16fadab1a34f28f6a0d16cf03766c054d47352b07952434182776535e578dbbd146db235b1c65b8fb76a49bcc + languageName: node + linkType: hard + "natural-compare-lite@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare-lite@npm:1.4.0" @@ -4872,6 +5181,13 @@ __metadata: languageName: node linkType: hard +"object-assign@npm:^4.0.1": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: f5cd1f2f1e82e12207e4f2377d9d7d90fbc0d9822a6afa717a6dcab6930d8925e1ebbbb25df770c31ff11335ee423459ba65ffa2e53999926c328b806b4d73d6 + languageName: node + linkType: hard + "once@npm:^1.3.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -5056,6 +5372,13 @@ __metadata: languageName: node linkType: hard +"pirates@npm:^4.0.1": + version: 4.0.6 + resolution: "pirates@npm:4.0.6" + checksum: e9d87a7cd0dc6d144ac558def9181e8d6cda8e840e147855c16735b9d8b2ebb7a04bd12a3dc6fe4c8b4f45d8b80ce6921657740fcaf5df931f355f13812aaf34 + languageName: node + linkType: hard + "pirates@npm:^4.0.4": version: 4.0.5 resolution: "pirates@npm:4.0.5" @@ -5072,6 +5395,24 @@ __metadata: languageName: node linkType: hard +"postcss-load-config@npm:^4.0.1": + version: 4.0.1 + resolution: "postcss-load-config@npm:4.0.1" + dependencies: + lilconfig: "npm:^2.0.5" + yaml: "npm:^2.1.1" + peerDependencies: + postcss: ">=8.0.9" + ts-node: ">=9.0.0" + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + checksum: 140d83311c39661ec16cc106bc2c8bcc34f0e67bf4f10206d7eeb43e04b70e1edfedab23163ab6cd7fb7ef8f1001b73e5c8098fbae279bbbf27b0d95bf2d7911 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -5326,6 +5667,20 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^3.2.5": + version: 3.28.0 + resolution: "rollup@npm:3.28.0" + dependencies: + fsevents: "npm:~2.3.2" + dependenciesMeta: + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: bbeca70998a2910d9acaea758116f48b79c58e696e501522f08c7a6bee9511a2498dd417d2a1decd8d4b6facc3e0662c6bc7897ae688e18d52a3201fbe3362f3 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -5474,6 +5829,15 @@ __metadata: languageName: node linkType: hard +"source-map@npm:0.8.0-beta.0": + version: 0.8.0-beta.0 + resolution: "source-map@npm:0.8.0-beta.0" + dependencies: + whatwg-url: "npm:^7.0.0" + checksum: 4bc71864ed618ad3a75194fee233aff938dd1716010e34ca8c33e3216a5977ebf56ae6cd1102f72be1a9a7388d25c55865f7710ba30ea5255e8713f38eae89b3 + languageName: node + linkType: hard + "source-map@npm:^0.6.0, source-map@npm:^0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" @@ -5580,6 +5944,24 @@ __metadata: languageName: node linkType: hard +"sucrase@npm:^3.20.3": + version: 3.34.0 + resolution: "sucrase@npm:3.34.0" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.2" + commander: "npm:^4.0.0" + glob: "npm:7.1.6" + lines-and-columns: "npm:^1.1.6" + mz: "npm:^2.7.0" + pirates: "npm:^4.0.1" + ts-interface-checker: "npm:^0.1.9" + bin: + sucrase: bin/sucrase + sucrase-node: bin/sucrase-node + checksum: 1ddbc13461d179c3ac2d8cf7ed8c9f95a075ea819fd5166b515c54dc8687db7592dd92e5a141ec8387b10ce4d0e908ace2ae5f21f5eb28ee115dd5297a9acccd + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -5646,6 +6028,24 @@ __metadata: languageName: node linkType: hard +"thenify-all@npm:^1.0.0": + version: 1.6.0 + resolution: "thenify-all@npm:1.6.0" + dependencies: + thenify: "npm:>= 3.1.0 < 4" + checksum: c04e83cf6b09741184d578ae73dfcd75566248f21bcf35aac2b9f90b8057b6bc5e401da12df1797cee3235a43113a6dcbd76a02532192a4da0a3007d94e8d6ef + languageName: node + linkType: hard + +"thenify@npm:>= 3.1.0 < 4": + version: 3.3.1 + resolution: "thenify@npm:3.3.1" + dependencies: + any-promise: "npm:^1.0.0" + checksum: 72ff962890b229a21c2c5cc022d105a265b9a3d631925efeba513fecefeb9a87ae6177dbe4befb7ddf78676f5f2a3320d1ed1a715c000da240807200a4e1a7d2 + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -5669,6 +6069,31 @@ __metadata: languageName: node linkType: hard +"tr46@npm:^1.0.1": + version: 1.0.1 + resolution: "tr46@npm:1.0.1" + dependencies: + punycode: "npm:^2.1.0" + checksum: 077551401b0752fb141ba39d6c287b3783d32ac5a054a0e991b084c888e47789857f2957199840c1f1529deb9c6b9cbd53ab836f3bfcad41411f430e8685ddd8 + languageName: node + linkType: hard + +"tree-kill@npm:^1.2.2": + version: 1.2.2 + resolution: "tree-kill@npm:1.2.2" + bin: + tree-kill: cli.js + checksum: e1c77812496ec255402297a3494acc4cda93d532ebefb1c6704b38d2a8eb6b9ed03d5f0b088a13341705f2923e52a73c3c2bb87a30ea890095cc51fb7a4ce6e0 + languageName: node + linkType: hard + +"ts-interface-checker@npm:^0.1.9": + version: 0.1.13 + resolution: "ts-interface-checker@npm:0.1.13" + checksum: 28232bd3fc685da7d80666cb0c3edd8b07530931b0e3e192572c91019f863e5a9f619c7e0b52f185e8277e8515e99b0915b2b2f161cd62e183acc731a915dee9 + languageName: node + linkType: hard + "tslib@npm:2.3.1": version: 2.3.1 resolution: "tslib@npm:2.3.1" @@ -5683,6 +6108,42 @@ __metadata: languageName: node linkType: hard +"tsup@npm:^7.2.0": + version: 7.2.0 + resolution: "tsup@npm:7.2.0" + dependencies: + bundle-require: "npm:^4.0.0" + cac: "npm:^6.7.12" + chokidar: "npm:^3.5.1" + debug: "npm:^4.3.1" + esbuild: "npm:^0.18.2" + execa: "npm:^5.0.0" + globby: "npm:^11.0.3" + joycon: "npm:^3.0.1" + postcss-load-config: "npm:^4.0.1" + resolve-from: "npm:^5.0.0" + rollup: "npm:^3.2.5" + source-map: "npm:0.8.0-beta.0" + sucrase: "npm:^3.20.3" + tree-kill: "npm:^1.2.2" + peerDependencies: + "@swc/core": ^1 + postcss: ^8.4.12 + typescript: ">=4.1.0" + peerDependenciesMeta: + "@swc/core": + optional: true + postcss: + optional: true + typescript: + optional: true + bin: + tsup: dist/cli-default.js + tsup-node: dist/cli-node.js + checksum: f8bd6a05042af09d63f78e4e83bfe1ade9448f8eea8bac51f9335683f74fd827acef8510f2d061970060d7a6f697b6ca88318c411583a7a6ad07032830579474 + languageName: node + linkType: hard + "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -5890,10 +6351,29 @@ __metadata: jose: "npm:>= 4.12.0 < 5.0.0" prettier: "npm:^2.8.4" rfc4648: "npm:^1.5.2" + tsup: "npm:^7.2.0" typescript: "npm:^4.9.5" languageName: unknown linkType: soft +"webidl-conversions@npm:^4.0.2": + version: 4.0.2 + resolution: "webidl-conversions@npm:4.0.2" + checksum: 68c1adc8200c122eeb9cd3ccb6407e929dab3108210a249ce485ac71acbe8d943cf97fe03687fe350295be467de1c0538d4ee0e0818267a941f1fbcdb0d8f765 + languageName: node + linkType: hard + +"whatwg-url@npm:^7.0.0": + version: 7.1.0 + resolution: "whatwg-url@npm:7.1.0" + dependencies: + lodash.sortby: "npm:^4.7.0" + tr46: "npm:^1.0.1" + webidl-conversions: "npm:^4.0.2" + checksum: 81485960495654692080d29ba6c311765eed40d8ea2227dc1a22609302d9091992255aabf6c17529a23efe3afae0527dd24f350895e25fbd2906225b1f389cbd + languageName: node + linkType: hard + "which@npm:^2.0.1, which@npm:^2.0.2": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -5985,6 +6465,13 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.1.1": + version: 2.3.1 + resolution: "yaml@npm:2.3.1" + checksum: f33e26b726061603f7a55870726cd5225a2868ea9accec41ff57f42e0398b1bdabf15b122ac724601fe41b978d5a46e7c3c43d3361f20f82047f054b2bf64621 + languageName: node + linkType: hard + "yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1"