├── .editorconfig
├── .github
├── FUNDING.yml
└── workflows
│ ├── pull_request.yml
│ └── pull_request_lint.yml
├── .gitignore
├── .prettierignore
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── .yarn
└── releases
│ └── yarn-4.0.0-rc.39.cjs
├── .yarnrc.yml
├── LICENSE
├── README.md
├── core
├── env.ts
├── error.ts
├── jwt.test.ts
├── jwt.ts
└── utils.ts
├── google
├── accessToken.test.ts
├── accessToken.ts
├── credentials.test.ts
├── credentials.ts
├── customToken.test.ts
├── customToken.ts
├── idToken.test.ts
├── idToken.ts
└── index.ts
├── index.ts
├── jest.config.js
├── package.json
├── test
├── env.ts
├── setup.ts
└── test.env
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 |
7 | [*.{js,json,yml}]
8 | charset = utf-8
9 | indent_style = space
10 | indent_size = 2
11 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: kriasoft
4 | patreon: koistya
5 | open_collective: react-starter-kit
6 |
--------------------------------------------------------------------------------
/.github/workflows/pull_request.yml:
--------------------------------------------------------------------------------
1 | # GitHub Actions workflow for Pull Requests
2 | # https://help.github.com/actions
3 |
4 | name: PR
5 |
6 | on: [pull_request]
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: actions/setup-node@v3
14 | with:
15 | node-version: 19
16 | cache: "yarn"
17 |
18 | # Install dependencies
19 | - run: yarn config set enableGlobalCache false
20 | - run: yarn install
21 |
22 | # Analyze code for potential problems
23 | - run: yarn prettier --check .
24 | - run: yarn lint
25 | - run: yarn tsc
26 |
27 | # Test
28 | - run: yarn test
29 | env:
30 | GOOGLE_CLOUD_CREDENTIALS: ${{ secrets.GOOGLE_CLOUD_CREDENTIALS }}
31 | FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }}
32 |
--------------------------------------------------------------------------------
/.github/workflows/pull_request_lint.yml:
--------------------------------------------------------------------------------
1 | # GitHub Actions workflow
2 | # https://help.github.com/actions
3 |
4 | name: "conventionalcommits.org"
5 |
6 | on:
7 | pull_request:
8 | types:
9 | - opened
10 | - edited
11 | - synchronize
12 |
13 | jobs:
14 | main:
15 | name: lint
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: amannn/action-semantic-pull-request@v4
19 | env:
20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Include your project-specific ignores in this file
2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files
3 |
4 | # Cache and compiled output
5 | /.cache/
6 | /dist/
7 |
8 | # Yarn package manager
9 | # https://yarnpkg.com/getting-started/qa/#which-files-should-be-gitignored
10 | /node_modules
11 | .pnp.*
12 | .yarn/*
13 | !.yarn/patches
14 | !.yarn/plugins
15 | !.yarn/releases
16 | !.yarn/sdks
17 | !.yarn/versions
18 |
19 | # Logs
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 | # Overrides for *.env files
24 | *.override.env
25 |
26 | # Visual Studio Code
27 | # https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
28 | .vscode/*
29 | !.vscode/settings.json
30 | !.vscode/tasks.json
31 | !.vscode/launch.json
32 | !.vscode/extensions.json
33 |
34 | # macOS
35 | # https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
36 | .DS_Store
37 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /.yarn
2 | /dist
3 | /node_modules
4 | /tsconfig.json
5 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "name": "vscode-jest-tests.v2",
10 | "request": "launch",
11 | "args": [
12 | "--runInBand",
13 | "--watchAll=false",
14 | "--testNamePattern",
15 | "${jest.testNamePattern}",
16 | "--runTestsByPath",
17 | "${jest.testFile}"
18 | ],
19 | "cwd": "${workspaceFolder}",
20 | "console": "integratedTerminal",
21 | "internalConsoleOptions": "neverOpen",
22 | "disableOptimisticBPs": true,
23 | "runtimeExecutable": "node",
24 | "runtimeArgs": [
25 | "--experimental-vm-modules",
26 | "${workspaceFolder}/node_modules/.bin/jest"
27 | ],
28 | "windows": {
29 | "runtimeArgs": [
30 | "--experimental-vm-modules",
31 | "${workspaceFolder}/node_modules/jest/bin/jest"
32 | ]
33 | }
34 | }
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.codeActionsOnSave": {
3 | "source.organizeImports": true
4 | },
5 | "editor.defaultFormatter": "esbenp.prettier-vscode",
6 | "editor.formatOnSave": true,
7 | "editor.tabSize": 2,
8 | "typescript.enablePromptUseWorkspaceTsdk": true,
9 | "jest.jestCommandLine": "yarn node --experimental-vm-modules $(yarn bin jest)",
10 | "jestrunner.jestCommand": "yarn node --experimental-vm-modules $(yarn bin jest)",
11 | "jestrunner.debugOptions": {
12 | "env": {
13 | "NODE_OPTIONS": "--experimental-vm-modules"
14 | }
15 | },
16 | "files.exclude": {
17 | "**/.cache": true,
18 | "**/.DS_Store": true,
19 | "**/.editorconfig": true,
20 | "**/.eslintcache": true,
21 | "**/.git": true,
22 | "**/.gitattributes": true,
23 | "**/.pnp.cjs": true,
24 | "**/.pnp.loader.mjs": true,
25 | "**/.prettierignore": true,
26 | "**/node_modules": true
27 | },
28 | "search.exclude": {
29 | "**/dist/": true,
30 | "**/yarn-error.log": true,
31 | "**/yarn.lock": true,
32 | "**/.yarn": true,
33 | "**/.pnp.*": true
34 | },
35 | "terminal.integrated.env.linux": {
36 | "CACHE_DIR": "${workspaceFolder}/.cache"
37 | },
38 | "terminal.integrated.env.osx": {
39 | "CACHE_DIR": "${workspaceFolder}/.cache"
40 | },
41 | "terminal.integrated.env.windows": {
42 | "CACHE_DIR": "${workspaceFolder}\\.cache"
43 | },
44 | "cSpell.words": [
45 | "async",
46 | "await",
47 | "cjs",
48 | "endregion",
49 | "esm",
50 | "hono",
51 | "identitytoolkit",
52 | "jest",
53 | "mjs",
54 | "node",
55 | "ts",
56 | "tsconfig",
57 | "tslib",
58 | "tslint",
59 | "tsnode",
60 | "tsv",
61 | "tsx",
62 | "yarn"
63 | ]
64 | }
65 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
3 | yarnPath: .yarn/releases/yarn-4.0.0-rc.39.cjs
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022-present Kriasoft
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Web Auth Library
2 |
3 | [](https://www.npmjs.com/package/web-auth-library)
4 | [](https://www.npmjs.com/package/web-auth-library)
5 | [](http://www.typescriptlang.org/)
6 | [](http://patreon.com/koistya)
7 | [](https://discord.gg/bSsv7XM)
8 |
9 | Authentication library for Google Cloud, Firebase, and other cloud providers that uses standard [Web Crypto API](https://developer.mozilla.org/docs/Web/API/Web_Crypto_API) and runs in different environments and runtimes, including but not limited to:
10 |
11 | - [Bun](https://bun.sh/)
12 | - [Browsers](https://developer.mozilla.org/docs/Web/API/Web_Crypto_API)
13 | - [Cloudflare Workers](https://workers.cloudflare.com/)
14 | - [Deno](https://deno.land/)
15 | - [Electron](https://www.electronjs.org/)
16 | - [Node.js](https://nodejs.org/)
17 | - [Vercel's Edge Runtime](https://edge-runtime.vercel.app/)
18 |
19 | It has minimum dependencies, small bundle size, and optimized for speed and performance.
20 |
21 | ## Getting Stated
22 |
23 | ```bash
24 | # Install using NPM
25 | $ npm install web-auth-library --save
26 |
27 | # Install using Yarn
28 | $ yarn add web-auth-library
29 | ```
30 |
31 | ## Usage Examples
32 |
33 | ### Verify the user ID Token issued by Google or Firebase
34 |
35 | **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.
36 |
37 | ```ts
38 | import { verifyIdToken } from "web-auth-library/google";
39 |
40 | const token = await verifyIdToken({
41 | idToken,
42 | credentials: env.GOOGLE_CLOUD_CREDENTIALS,
43 | });
44 |
45 | // => {
46 | // iss: 'https://securetoken.google.com/example',
47 | // aud: 'example',
48 | // auth_time: 1677525930,
49 | // user_id: 'temp',
50 | // sub: 'temp',
51 | // iat: 1677525930,
52 | // exp: 1677529530,
53 | // firebase: {}
54 | // }
55 | ```
56 |
57 | ### Create an access token for accessing [Google Cloud APIs](https://developers.google.com/apis-explorer)
58 |
59 | ```ts
60 | import { getAccessToken } from "web-auth-library/google";
61 |
62 | // Generate a short lived access token from the service account key credentials
63 | const accessToken = await getAccessToken({
64 | credentials: env.GOOGLE_CLOUD_CREDENTIALS,
65 | scope: "https://www.googleapis.com/auth/cloud-platform",
66 | });
67 |
68 | // Make a request to one of the Google's APIs using that token
69 | const res = await fetch(
70 | "https://cloudresourcemanager.googleapis.com/v1/projects",
71 | {
72 | headers: { Authorization: `Bearer ${accessToken}` },
73 | }
74 | );
75 | ```
76 |
77 | ## Create a custom ID token using Service Account credentials
78 |
79 | ```ts
80 | import { getIdToken } from "web-auth-library/google";
81 |
82 | const idToken = await getIdToken({
83 | credentials: env.GOOGLE_CLOUD_CREDENTIALS,
84 | audience: "https://example.com",
85 | });
86 | ```
87 |
88 | ## An alternative way passing credentials
89 |
90 | 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:
91 |
92 | ```ts
93 | import { verifyIdToken } from "web-auth-library/google";
94 |
95 | const env = { GOOGLE_CLOUD_CREDENTIALS: "..." };
96 | const token = await verifyIdToken({ idToken, env });
97 | ```
98 |
99 | ## Optimize cache renewal background tasks
100 |
101 | Pass the optional `waitUntil(promise)` function provided by the target runtime to optimize the way authentication tokens are being renewed in background. For example, using Cloudflare Workers and [Hono.js](https://hono.dev/):
102 |
103 | ```ts
104 | import { Hono } from "hono";
105 | import { verifyIdToken } from "web-auth-library/google";
106 |
107 | const app = new Hono();
108 |
109 | app.get("/", ({ env, executionCtx, json }) => {
110 | const idToken = await verifyIdToken({
111 | idToken: "...",
112 | waitUntil: executionCtx.waitUntil,
113 | env,
114 | });
115 |
116 | return json({ ... });
117 | })
118 | ```
119 |
120 | ## Backers 💰
121 |
122 |
123 |
124 | ## Related Projects
125 |
126 | - [React Starter Kit](https://github.com/kriasoft/react-starter-kit) — front-end template for React and Relay using Jamstack architecture
127 | - [GraphQL API and Relay Starter Kit](https://github.com/kriasoft/graphql-starter) — monorepo template, pre-configured with GraphQL API, React, and Relay
128 | - [Cloudflare Workers Starter Kit](https://github.com/kriasoft/cloudflare-starter-kit) — TypeScript project template for Cloudflare Workers
129 |
130 | ## How to Contribute
131 |
132 | You're very welcome to [create a PR](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request)
133 | or send me a message on [Discord](https://discord.gg/bSsv7XM).
134 |
135 | In order to unit test this library locally you will need [Node.js](https://nodejs.org/) v18+ with [corepack enabled](https://nodejs.org/api/corepack.html), a Google Cloud [service account key](https://cloud.google.com/iam/docs/keys-create-delete) ([here](https://console.cloud.google.com/iam-admin/serviceaccounts)) and Firebase API Key ([here](https://console.cloud.google.com/apis/credentials)) that you can save into the [`test/test.override.env`](./test/test.env) file, for example:
136 |
137 | ```
138 | GOOGLE_CLOUD_PROJECT=example
139 | GOOGLE_CLOUD_CREDENTIALS={"type":"service_account","project_id":"example",...}
140 | FIREBASE_API_KEY=AIzaSyAZEmdfRWvEYgZpwm6EBLkYJf6ySIMF3Hy
141 | ```
142 |
143 | Then run unit tests via `yarn test [--watch]`.
144 |
145 | ## License
146 |
147 | Copyright © 2022-present Kriasoft. This source code is licensed under the MIT license found in the
148 | [LICENSE](https://github.com/kriasoft/web-auth-library/blob/main/LICENSE) file.
149 |
150 | ---
151 |
152 | Made with ♥ by Konstantin Tarkus ([@koistya](https://twitter.com/koistya), [blog](https://medium.com/@koistya))
153 | and [contributors](https://github.com/kriasoft/web-auth-library/graphs/contributors).
154 |
--------------------------------------------------------------------------------
/core/env.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | export const canUseDefaultCache =
5 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
6 | typeof (globalThis as any).caches?.default?.put === "function";
7 |
--------------------------------------------------------------------------------
/core/error.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | export class FetchError extends Error {
5 | readonly name: string = "FetchError";
6 | readonly response: Response;
7 |
8 | constructor(
9 | message: string,
10 | options: { response: Response; cause?: unknown }
11 | ) {
12 | super(message, { cause: options?.cause });
13 | this.response = options.response;
14 |
15 | if (Error.captureStackTrace) {
16 | Error.captureStackTrace(this, Error);
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/core/jwt.test.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | import { jwt } from "../index.js";
5 |
6 | test("jwt.decode(token)", () => {
7 | const token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJleHAiOjEzOTMyODY4OTMsImlhdCI6MTM5MzI2ODg5M30.4-iaDojEVl0pJQMjrbM1EzUIfAZgsbK_kgnVyVxFSVo"; // prettier-ignore
8 | const result = jwt.decode(token);
9 |
10 | expect(result).toMatchInlineSnapshot(`
11 | {
12 | "data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJleHAiOjEzOTMyODY4OTMsImlhdCI6MTM5MzI2ODg5M30",
13 | "header": {
14 | "alg": "HS256",
15 | "typ": "JWT",
16 | },
17 | "payload": {
18 | "exp": 1393286893,
19 | "foo": "bar",
20 | "iat": 1393268893,
21 | },
22 | "signature": "4-iaDojEVl0pJQMjrbM1EzUIfAZgsbK_kgnVyVxFSVo",
23 | }
24 | `);
25 | });
26 |
27 | test("jwt.decode(unicodeToken)", () => {
28 | const unicodeToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9zw6kiLCJpYXQiOjE0MjU2NDQ5NjZ9.1CfFtdGUPs6q8kT3OGQSVlhEMdbuX0HfNSqum0023a0"; // prettier-ignore
29 | const result = jwt.decode(unicodeToken);
30 |
31 | expect(result).toMatchInlineSnapshot(`
32 | {
33 | "data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9zw6kiLCJpYXQiOjE0MjU2NDQ5NjZ9",
34 | "header": {
35 | "alg": "HS256",
36 | "typ": "JWT",
37 | },
38 | "payload": {
39 | "iat": 1425644966,
40 | "name": "José",
41 | },
42 | "signature": "1CfFtdGUPs6q8kT3OGQSVlhEMdbuX0HfNSqum0023a0",
43 | }
44 | `);
45 | });
46 |
47 | test("jwt.decode(binaryToken)", () => {
48 | const binaryToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9z6SIsImlhdCI6MTQyNTY0NDk2Nn0.cpnplCBxiw7Xqz5thkqs4Mo_dymvztnI0CI4BN0d1t8"; // prettier-ignore
49 | const result = jwt.decode(binaryToken);
50 |
51 | expect(result).toMatchInlineSnapshot(`
52 | {
53 | "data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9z6SIsImlhdCI6MTQyNTY0NDk2Nn0",
54 | "header": {
55 | "alg": "HS256",
56 | "typ": "JWT",
57 | },
58 | "payload": {
59 | "iat": 1425644966,
60 | "name": "Jos�",
61 | },
62 | "signature": "cpnplCBxiw7Xqz5thkqs4Mo_dymvztnI0CI4BN0d1t8",
63 | }
64 | `);
65 | });
66 |
--------------------------------------------------------------------------------
/core/jwt.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | import { base64url } from "rfc4648";
5 |
6 | /**
7 | * Converts the given JSON Web Token string into a `Jwt` object.
8 | */
9 | function decode(token: string): Jwt {
10 | const segments = token.split(".");
11 | const dec = new TextDecoder();
12 |
13 | if (segments.length !== 3) {
14 | throw new Error();
15 | }
16 |
17 | return {
18 | header: JSON.parse(
19 | dec.decode(base64url.parse(segments[0], { loose: true }))
20 | ),
21 |
22 | payload: JSON.parse(
23 | dec.decode(base64url.parse(segments[1], { loose: true }))
24 | ),
25 |
26 | data: `${segments[0]}.${segments[1]}`,
27 | signature: segments[2],
28 | };
29 | }
30 |
31 | async function verify(
32 | token: Jwt | string,
33 | options: VerifyOptions
34 | ): Promise {
35 | const enc = new TextEncoder();
36 | const jwt = typeof token === "string" ? decode(token) : token;
37 | const aud = (jwt.payload as { aud?: string }).aud;
38 |
39 | if (
40 | options.audience &&
41 | (!aud ||
42 | (Array.isArray(options.audience) && !options.audience.includes(aud)) ||
43 | options.audience !== aud)
44 | ) {
45 | return;
46 | }
47 |
48 | const verified = await crypto.subtle.verify(
49 | options.key.algorithm,
50 | options.key,
51 | base64url.parse(jwt.signature, { loose: true }),
52 | enc.encode(jwt.data)
53 | );
54 |
55 | return verified ? jwt.payload : undefined;
56 | }
57 |
58 | /* ------------------------------------------------------------------------------- *
59 | * TypeScript definitions
60 | * ------------------------------------------------------------------------------- */
61 |
62 | /**
63 | * Identifies which algorithm is used to generate the signature.
64 | */
65 | interface JwtHeader {
66 | /** Token type */
67 | typ?: string;
68 | /** Content type*/
69 | cty?: string;
70 | /** Message authentication code algorithm */
71 | alg?: string;
72 | /** Key ID */
73 | kid?: string;
74 | /** x.509 Certificate Chain */
75 | x5c?: string;
76 | /** x.509 Certificate Chain URL */
77 | x5u?: string;
78 | /** Critical */
79 | crit?: string;
80 | }
81 |
82 | /**
83 | * Contains a set of claims.
84 | */
85 | interface JwtPayload {
86 | /** Issuer */
87 | iss?: string;
88 | /** Subject */
89 | sub?: string;
90 | /** Audience */
91 | aud?: string;
92 | /** Authorized party */
93 | azp?: string;
94 | /** Expiration time */
95 | exp?: number;
96 | /** Not before */
97 | nbf?: number;
98 | /** Issued at */
99 | iat?: number;
100 | /** JWT ID */
101 | jti?: string;
102 | }
103 |
104 | /**
105 | * JSON Web Token (JWT)
106 | */
107 | type Jwt = {
108 | header: H;
109 | payload: T;
110 | data: string;
111 | signature: string;
112 | };
113 |
114 | type VerifyOptions = {
115 | key: CryptoKey;
116 | audience?: string[] | string;
117 | };
118 |
119 | export {
120 | decode,
121 | verify,
122 | type Jwt,
123 | type JwtHeader,
124 | type JwtPayload,
125 | type VerifyOptions,
126 | };
127 |
--------------------------------------------------------------------------------
/core/utils.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | const logOnceKeys = new Set();
5 | type Severity = "log" | "warn" | "error";
6 |
7 | export function logOnce(severity: Severity, key: string, message: string) {
8 | if (!logOnceKeys.has(key)) {
9 | logOnceKeys.add(key);
10 | console[severity](message);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/google/accessToken.test.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | import { decodeJwt } from "jose";
5 | import env from "../test/env.js";
6 | import { getAccessToken } from "./accessToken.js";
7 |
8 | test("getAccessToken({ credentials, scope })", async () => {
9 | const accessToken = await getAccessToken({
10 | credentials: env.GOOGLE_CLOUD_CREDENTIALS,
11 | scope: "https://www.googleapis.com/auth/cloud-platform",
12 | });
13 |
14 | expect(accessToken?.substring(0, 30)).toEqual(
15 | expect.stringContaining("ya29.c.")
16 | );
17 | });
18 |
19 | test("getAccessToken({ credentials, audience })", async () => {
20 | const idToken = await getAccessToken({
21 | credentials: env.GOOGLE_CLOUD_CREDENTIALS,
22 | audience: "https://example.com",
23 | });
24 |
25 | expect(idToken?.substring(0, 30)).toEqual(
26 | expect.stringContaining("eyJhbGciOi")
27 | );
28 |
29 | expect(decodeJwt(idToken)).toEqual(
30 | expect.objectContaining({
31 | aud: "https://example.com",
32 | email_verified: true,
33 | iss: "https://accounts.google.com",
34 | })
35 | );
36 | });
37 |
--------------------------------------------------------------------------------
/google/accessToken.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | import { decodeJwt } from "jose";
5 | import { canUseDefaultCache } from "../core/env.js";
6 | import { FetchError } from "../core/error.js";
7 | import { logOnce } from "../core/utils.js";
8 | import { getCredentials, type Credentials } from "./credentials.js";
9 | import { createCustomToken } from "./customToken.js";
10 |
11 | const defaultCache = new Map();
12 |
13 | /**
14 | * Fetches an access token from Google Cloud API using the provided
15 | * service account credentials.
16 | *
17 | * @throws {FetchError} — If the access token could not be fetched.
18 | */
19 | export async function getAccessToken(options: Options) {
20 | if (!options?.waitUntil && canUseDefaultCache) {
21 | logOnce("warn", "verifyIdToken", "Missing `waitUntil` option.");
22 | }
23 |
24 | let credentials: Credentials;
25 |
26 | // Normalize service account credentials
27 | // using env.GOOGLE_CLOUD_CREDENTIALS as a fallback
28 | if (options?.credentials) {
29 | credentials = getCredentials(options.credentials);
30 | } else {
31 | if (!options?.env?.GOOGLE_CLOUD_CREDENTIALS) {
32 | throw new TypeError("Missing credentials");
33 | }
34 | credentials = getCredentials(options.env.GOOGLE_CLOUD_CREDENTIALS);
35 | }
36 |
37 | // Normalize authentication scope and audience values
38 | const scope = Array.isArray(options.scope)
39 | ? options.scope.join(",")
40 | : options.scope;
41 | const audience = Array.isArray(options.audience)
42 | ? options.audience.join(",")
43 | : options.audience;
44 |
45 | const tokenUrl = credentials.token_uri;
46 |
47 | // Create a cache key that can be used with Cloudflare Cache API
48 | const cacheKeyUrl = new URL(tokenUrl);
49 | cacheKeyUrl.searchParams.set("scope", scope ?? "");
50 | cacheKeyUrl.searchParams.set("aud", audience ?? "");
51 | cacheKeyUrl.searchParams.set("key", credentials.private_key_id);
52 | const cacheKey = cacheKeyUrl.toString();
53 |
54 | // Attempt to retrieve the token from the cache
55 | const cache: Map = options.cache ?? defaultCache;
56 | const cacheValue = cache.get(cacheKey);
57 | let now = Math.floor(Date.now() / 1000);
58 |
59 | if (cacheValue) {
60 | if (cacheValue.created > now - 60 * 60) {
61 | let token = await cacheValue.promise;
62 |
63 | if (token.expires > now) {
64 | return token.token;
65 | } else {
66 | const nextValue = cache.get(cacheKey);
67 |
68 | if (nextValue && nextValue !== cacheValue) {
69 | token = await nextValue.promise;
70 | if (token.expires > now) {
71 | return token.token;
72 | } else {
73 | cache.delete(cacheKey);
74 | }
75 | }
76 | }
77 | } else {
78 | cache.delete(cacheKey);
79 | }
80 | }
81 |
82 | const promise = (async () => {
83 | let res: Response | undefined;
84 |
85 | // Attempt to retrieve the token from Cloudflare cache
86 | // if the code is running in Cloudflare Workers environment
87 | if (canUseDefaultCache) {
88 | res = await caches.default.match(cacheKey);
89 | }
90 |
91 | if (!res) {
92 | now = Math.floor(Date.now() / 1000);
93 |
94 | // Request a new token from the Google Cloud API
95 | const jwt = await createCustomToken({
96 | credentials,
97 | scope: options.audience ?? options.scope,
98 | });
99 | const body = new URLSearchParams();
100 | body.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
101 | body.append("assertion", jwt);
102 | res = await fetch(tokenUrl, {
103 | method: "POST",
104 | headers: { "Content-Type": "application/x-www-form-urlencoded" },
105 | body,
106 | });
107 |
108 | if (!res.ok) {
109 | const error = await res
110 | .json<{ error_description?: string }>()
111 | .then((data) => data?.error_description)
112 | .catch(() => undefined);
113 | throw new FetchError(error ?? "Failed to fetch an access token.", {
114 | response: res,
115 | });
116 | }
117 |
118 | if (canUseDefaultCache) {
119 | let cacheRes = res.clone();
120 | cacheRes = new Response(cacheRes.body, cacheRes);
121 | cacheRes.headers.set("Cache-Control", `max-age=3590, public`);
122 | cacheRes.headers.set("Last-Modified", new Date().toUTCString());
123 | const cachePromise = caches.default.put(cacheKey, cacheRes);
124 |
125 | if (options.waitUntil) {
126 | options.waitUntil(cachePromise);
127 | }
128 | }
129 | }
130 |
131 | const data = await res.json();
132 |
133 | if ("id_token" in data) {
134 | const claims = decodeJwt(data.id_token);
135 | return { token: data.id_token, expires: claims.exp as number };
136 | }
137 |
138 | const lastModified = res.headers.get("last-modified");
139 | const expires = lastModified
140 | ? Math.floor(new Date(lastModified).valueOf() / 1000) + data.expires_in
141 | : now + data.expires_in;
142 |
143 | return { expires, token: data.access_token };
144 | })();
145 |
146 | cache.set(cacheKey, { created: now, promise });
147 | return await promise.then((data) => data.token);
148 | }
149 |
150 | // #region Types
151 |
152 | type Options = {
153 | /**
154 | * Google Cloud service account credentials.
155 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys
156 | * @default env.GOOGLE_CLOUD_PROJECT
157 | */
158 | credentials: Credentials | string;
159 | /**
160 | * Authentication scope(s).
161 | */
162 | scope?: string[] | string;
163 | /**
164 | * Recipients that the ID token should be issued for.
165 | */
166 | audience?: string[] | string;
167 | env?: {
168 | /**
169 | * Google Cloud project ID.
170 | */
171 | GOOGLE_CLOUD_PROJECT?: string;
172 | /**
173 | * Google Cloud service account credentials.
174 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys
175 | */
176 | GOOGLE_CLOUD_CREDENTIALS: string;
177 | };
178 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
179 | waitUntil?: (promise: Promise) => void;
180 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
181 | cache?: Map;
182 | };
183 |
184 | type TokenResponse =
185 | | {
186 | access_token: string;
187 | expires_in: number;
188 | token_type: string;
189 | }
190 | | {
191 | id_token: string;
192 | };
193 |
194 | type CacheValue = {
195 | created: number;
196 | promise: Promise<{ token: string; expires: number }>;
197 | };
198 |
199 | // #endregion
200 |
--------------------------------------------------------------------------------
/google/credentials.test.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | import env from "../test/env.js";
5 | import {
6 | getCredentials,
7 | getPrivateKey,
8 | importPublicKey,
9 | } from "./credentials.js";
10 |
11 | test("getPrivateKey({ credentials })", async () => {
12 | const privateKey = await getPrivateKey({
13 | credentials: env.GOOGLE_CLOUD_CREDENTIALS,
14 | });
15 |
16 | expect(privateKey).toEqual(
17 | expect.objectContaining({
18 | algorithm: expect.objectContaining({
19 | hash: { name: "SHA-256" },
20 | modulusLength: 2048,
21 | name: "RSASSA-PKCS1-v1_5",
22 | }),
23 | })
24 | );
25 | });
26 |
27 | test("importPublicKey({ keyId, certificateURL })", async () => {
28 | const credentials = getCredentials(env.GOOGLE_CLOUD_CREDENTIALS);
29 | const privateKey = await importPublicKey({
30 | keyId: credentials.private_key_id,
31 | certificateURL: credentials.client_x509_cert_url,
32 | });
33 |
34 | expect(privateKey).toEqual(
35 | expect.objectContaining({
36 | algorithm: expect.objectContaining({
37 | hash: { name: "SHA-256" },
38 | modulusLength: 2048,
39 | name: "RSASSA-PKCS1-v1_5",
40 | }),
41 | })
42 | );
43 | });
44 |
--------------------------------------------------------------------------------
/google/credentials.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | import { importPKCS8, importX509, KeyLike } from "jose";
5 | import { FetchError } from "../core/error.js";
6 |
7 | const inFlight = new Map>();
8 | const cache = new Map();
9 |
10 | /**
11 | * Normalizes Google Cloud Platform (GCP) service account credentials.
12 | */
13 | export function getCredentials(credentials: Credentials | string): Credentials {
14 | return typeof credentials === "string" || credentials instanceof String
15 | ? Object.freeze(JSON.parse(credentials as string))
16 | : Object.isFrozen(credentials)
17 | ? credentials
18 | : Object.freeze(credentials);
19 | }
20 |
21 | /**
22 | * Imports a private key from the provided Google Cloud (GCP)
23 | * service account credentials.
24 | */
25 | export function getPrivateKey(options: { credentials: Credentials | string }) {
26 | const credentials = getCredentials(options.credentials);
27 | return importPKCS8(credentials.private_key, "RS256");
28 | }
29 |
30 | /**
31 | * Imports a public key for the provided Google Cloud (GCP)
32 | * service account credentials.
33 | *
34 | * @throws {FetchError} - If the X.509 certificate could not be fetched.
35 | */
36 | export async function importPublicKey(options: {
37 | /**
38 | * Public key ID (kid).
39 | */
40 | keyId: string;
41 | /**
42 | * The X.509 certificate URL.
43 | * @default "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"
44 | */
45 | certificateURL?: string;
46 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
47 | waitUntil?: (promise: Promise) => void;
48 | }) {
49 | const keyId = options.keyId;
50 | const certificateURL = options.certificateURL ?? "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"; // prettier-ignore
51 | const cacheKey = `${certificateURL}?key=${keyId}`;
52 | const value = cache.get(cacheKey);
53 | const now = Date.now();
54 |
55 | async function fetchKey() {
56 | // Fetch the public key from Google's servers
57 | const res = await fetch(certificateURL);
58 |
59 | if (!res.ok) {
60 | const error = await res
61 | .json<{ error: { message: string } }>()
62 | .then((data) => data.error.message)
63 | .catch(() => undefined);
64 | throw new FetchError(error ?? "Failed to fetch the public key", {
65 | response: res,
66 | });
67 | }
68 |
69 | const data = await res.json>();
70 | const x509 = data[keyId];
71 |
72 | if (!x509) {
73 | throw new FetchError(`Public key "${keyId}" not found.`, {
74 | response: res,
75 | });
76 | }
77 |
78 | const key = await importX509(x509, "RS256");
79 |
80 | // Resolve the expiration time of the key
81 | const maxAge = res.headers.get("cache-control")?.match(/max-age=(\d+)/)?.[1]; // prettier-ignore
82 | const expires = Date.now() + Number(maxAge ?? "3600") * 1000;
83 |
84 | // Update the local cache
85 | cache.set(cacheKey, { key, expires });
86 | inFlight.delete(keyId);
87 |
88 | return key;
89 | }
90 |
91 | // Attempt to read the key from the local cache
92 | if (value) {
93 | if (value.expires > now + 10_000) {
94 | // If the key is about to expire, start a new request in the background
95 | if (value.expires - now < 600_000) {
96 | const promise = fetchKey();
97 | inFlight.set(cacheKey, promise);
98 | if (options.waitUntil) {
99 | options.waitUntil(promise);
100 | }
101 | }
102 | return value.key;
103 | } else {
104 | cache.delete(cacheKey);
105 | }
106 | }
107 |
108 | // Check if there is an in-flight request for the same key ID
109 | let promise = inFlight.get(cacheKey);
110 |
111 | // If not, start a new request
112 | if (!promise) {
113 | promise = fetchKey();
114 | inFlight.set(cacheKey, promise);
115 | }
116 |
117 | return await promise;
118 | }
119 |
120 | /**
121 | * Service account credentials for Google Cloud Platform (GCP).
122 | *
123 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys
124 | */
125 | export type Credentials = {
126 | type: string;
127 | project_id: string;
128 | private_key_id: string;
129 | private_key: string;
130 | client_id: string;
131 | client_email: string;
132 | auth_uri: string;
133 | token_uri: string;
134 | auth_provider_x509_cert_url: string;
135 | client_x509_cert_url: string;
136 | };
137 |
--------------------------------------------------------------------------------
/google/customToken.test.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | import { decodeJwt } from "jose";
5 | import env from "../test/env.js";
6 | import { createCustomToken } from "./customToken.js";
7 |
8 | test("createCustomToken({ credentials, scope })", async () => {
9 | const customToken = await createCustomToken({
10 | credentials: env.GOOGLE_CLOUD_CREDENTIALS,
11 | scope: "https://www.example.com",
12 | });
13 |
14 | expect(customToken?.substring(0, 30)).toEqual(
15 | expect.stringContaining("eyJhbGciOi")
16 | );
17 |
18 | expect(decodeJwt(customToken)).toEqual(
19 | expect.objectContaining({
20 | iss: expect.stringMatching(/\.iam\.gserviceaccount\.com$/),
21 | aud: "https://oauth2.googleapis.com/token",
22 | scope: "https://www.example.com",
23 | iat: expect.any(Number),
24 | exp: expect.any(Number),
25 | })
26 | );
27 | });
28 |
29 | test("createCustomToken({ credentials, scope: scopes })", async () => {
30 | const customToken = await createCustomToken({
31 | credentials: env.GOOGLE_CLOUD_CREDENTIALS,
32 | scope: ["https://www.example.com", "https://beta.example.com"],
33 | });
34 |
35 | expect(customToken?.substring(0, 30)).toEqual(
36 | expect.stringContaining("eyJhbGciOi")
37 | );
38 |
39 | expect(decodeJwt(customToken)).toEqual(
40 | expect.objectContaining({
41 | iss: expect.stringMatching(/\.iam\.gserviceaccount\.com$/),
42 | aud: "https://oauth2.googleapis.com/token",
43 | scope: "https://www.example.com https://beta.example.com",
44 | iat: expect.any(Number),
45 | exp: expect.any(Number),
46 | })
47 | );
48 | });
49 |
50 | test("createCustomToken({ env, scope })", async () => {
51 | const customToken = await createCustomToken({
52 | scope: "https://www.googleapis.com/auth/cloud-platform",
53 | env: { GOOGLE_CLOUD_CREDENTIALS: env.GOOGLE_CLOUD_CREDENTIALS },
54 | });
55 |
56 | expect(customToken?.substring(0, 30)).toEqual(
57 | expect.stringContaining("eyJhbGciOi")
58 | );
59 |
60 | expect(decodeJwt(customToken)).toEqual(
61 | expect.objectContaining({
62 | iss: expect.stringMatching(/\.iam\.gserviceaccount\.com$/),
63 | aud: "https://oauth2.googleapis.com/token",
64 | scope: "https://www.googleapis.com/auth/cloud-platform",
65 | iat: expect.any(Number),
66 | exp: expect.any(Number),
67 | })
68 | );
69 | });
70 |
71 | test("createCustomToken({ env, scope })", async () => {
72 | const promise = createCustomToken({
73 | scope: "https://www.googleapis.com/auth/cloud-platform",
74 | });
75 | expect(promise).rejects.toThrow(new TypeError("Missing credentials"));
76 | });
77 |
--------------------------------------------------------------------------------
/google/customToken.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | import { SignJWT } from "jose";
5 | import {
6 | getCredentials,
7 | getPrivateKey,
8 | type Credentials,
9 | } from "./credentials.js";
10 |
11 | /**
12 | * Generates a custom authentication token (JWT)
13 | * from a Google Cloud (GCP) service account key.
14 | *
15 | * @example
16 | * const customToken = await createCustomToken({
17 | * credentials: env.GOOGLE_CLOUD_CREDENTIALS,
18 | * scope: "https://www.googleapis.com/auth/cloud-platform",
19 | * });
20 | *
21 | * @example
22 | * const customToken = await createCustomToken({
23 | * env: { GOOGLE_CLOUD_CREDENTIALS: "..." },
24 | * scope: "https://www.example.com",
25 | * });
26 | */
27 | export async function createCustomToken(options: {
28 | /**
29 | * Google Cloud service account credentials.
30 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys
31 | * @default env.GOOGLE_CLOUD_PROJECT
32 | */
33 | credentials?: Credentials | string;
34 | /**
35 | * Authentication scope.
36 | * @example "https://www.googleapis.com/auth/cloud-platform"
37 | */
38 | scope?: string | string[];
39 | /**
40 | * The principal that is the subject of the JWT.
41 | */
42 | subject?: string;
43 | /**
44 | * The recipient(s) that the JWT is intended for.
45 | */
46 | audience?: string | string[];
47 | /**
48 | * Any other JWT clams.
49 | */
50 | [propName: string]: unknown;
51 | /**
52 | * Alternatively, you can pass credentials via the environment variable.
53 | */
54 | env?: {
55 | /**
56 | * Google Cloud service account credentials.
57 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys
58 | */
59 | GOOGLE_CLOUD_CREDENTIALS: string;
60 | };
61 | }) {
62 | /* eslint-disable-next-line prefer-const */
63 | let { credentials, scope, subject, audience, env, ...payload } = options;
64 |
65 | // Normalize credentials using env.GOOGLE_CLOUD_CREDENTIALS as a fallback
66 | if (credentials) {
67 | credentials = getCredentials(credentials);
68 | } else {
69 | if (!env?.GOOGLE_CLOUD_CREDENTIALS) {
70 | throw new TypeError("Missing credentials");
71 | }
72 | credentials = getCredentials(env.GOOGLE_CLOUD_CREDENTIALS);
73 | }
74 |
75 | // Normalize authentication scope (needs to be a string)
76 | scope = Array.isArray(scope) ? scope.join(" ") : scope;
77 |
78 | // Generate and sign a custom JWT token
79 | const privateKey = await getPrivateKey({ credentials });
80 | const customToken = await new SignJWT({ scope, ...payload })
81 | .setIssuer(credentials.client_email)
82 | .setAudience(audience ?? credentials.token_uri)
83 | .setSubject(subject ?? credentials.client_email)
84 | .setProtectedHeader({ alg: "RS256" })
85 | .setIssuedAt()
86 | .setExpirationTime("1h")
87 | .sign(privateKey);
88 |
89 | return customToken;
90 | }
91 |
--------------------------------------------------------------------------------
/google/idToken.test.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | import { decodeJwt } from "jose";
5 | import env from "../test/env.js";
6 | import { getIdToken, verifyIdToken } from "./idToken.js";
7 |
8 | test("getIdToken({ uid, apiKey, projectId, credentials })", async () => {
9 | const token = await getIdToken({
10 | uid: "temp",
11 | claims: { foo: "bar" },
12 | apiKey: env.FIREBASE_API_KEY,
13 | credentials: env.GOOGLE_CLOUD_CREDENTIALS,
14 | });
15 |
16 | expect(token).toEqual(
17 | expect.objectContaining({
18 | kind: "identitytoolkit#VerifyCustomTokenResponse",
19 | idToken: expect.stringMatching(/^eyJhbGciOiJSUzI1NiIs/),
20 | refreshToken: expect.any(String),
21 | expiresIn: "3600",
22 | isNewUser: expect.any(Boolean),
23 | })
24 | );
25 |
26 | expect(decodeJwt(token.idToken)).toEqual(
27 | expect.objectContaining({
28 | sub: "temp",
29 | user_id: "temp",
30 | aud: env.GOOGLE_CLOUD_PROJECT,
31 | iss: `https://securetoken.google.com/${env.GOOGLE_CLOUD_PROJECT}`,
32 | iat: expect.any(Number),
33 | exp: expect.any(Number),
34 | auth_time: expect.any(Number),
35 | })
36 | );
37 | });
38 |
39 | test("verifyIdToken({ idToken })", async () => {
40 | const { idToken } = await getIdToken({
41 | uid: "temp",
42 | apiKey: env.FIREBASE_API_KEY,
43 | credentials: env.GOOGLE_CLOUD_CREDENTIALS,
44 | });
45 | const token = await verifyIdToken({ idToken, env });
46 |
47 | expect(token).toEqual(
48 | expect.objectContaining({
49 | aud: env.GOOGLE_CLOUD_PROJECT,
50 | iss: `https://securetoken.google.com/${env.GOOGLE_CLOUD_PROJECT}`,
51 | sub: "temp",
52 | user_id: "temp",
53 | iat: expect.any(Number),
54 | exp: expect.any(Number),
55 | })
56 | );
57 | });
58 |
--------------------------------------------------------------------------------
/google/idToken.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | import { decodeProtectedHeader, errors, jwtVerify } from "jose";
5 | import { canUseDefaultCache } from "../core/env.js";
6 | import { FetchError } from "../core/error.js";
7 | import { logOnce } from "../core/utils.js";
8 | import { Credentials, getCredentials, importPublicKey } from "./credentials.js";
9 | import { createCustomToken } from "./customToken.js";
10 |
11 | /**
12 | * Creates a User ID token using Google Cloud service account credentials.
13 | */
14 | export async function getIdToken(options: {
15 | /**
16 | * User ID.
17 | */
18 | uid: string;
19 | /**
20 | * Additional user claims.
21 | */
22 | claims?: Record;
23 | /**
24 | * Google Cloud API key.
25 | * @see https://console.cloud.google.com/apis/credentials
26 | * @default env.FIREBASE_API_KEY
27 | */
28 | apiKey?: string;
29 | /**
30 | * Google Cloud project ID.
31 | * @default env.GOOGLE_CLOUD_PROJECT;
32 | */
33 | projectId?: string;
34 | /**
35 | * Google Cloud service account credentials.
36 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys
37 | * @default env.GOOGLE_CLOUD_PROJECT
38 | */
39 | credentials?: Credentials | string;
40 | /**
41 | * Alternatively, you can pass credentials via the environment variable.
42 | */
43 | env?: {
44 | /**
45 | * Google Cloud API key.
46 | * @see https://console.cloud.google.com/apis/credentials
47 | */
48 | FIREBASE_API_KEY: string;
49 | /**
50 | * Google Cloud project ID.
51 | */
52 | GOOGLE_CLOUD_PROJECT: string;
53 | /**
54 | * Google Cloud service account credentials.
55 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys
56 | */
57 | GOOGLE_CLOUD_CREDENTIALS: string;
58 | };
59 | }) {
60 | const uid = options?.uid;
61 |
62 | if (!uid) {
63 | throw new TypeError("Missing uid");
64 | }
65 |
66 | let apiKey = options?.apiKey;
67 |
68 | if (!apiKey) {
69 | if (options?.env?.FIREBASE_API_KEY) {
70 | apiKey = options.env.FIREBASE_API_KEY;
71 | } else {
72 | throw new TypeError("Missing apiKey");
73 | }
74 | }
75 |
76 | let credentials = options?.credentials;
77 |
78 | if (credentials) {
79 | credentials = getCredentials(credentials);
80 | } else {
81 | if (options?.env?.GOOGLE_CLOUD_CREDENTIALS) {
82 | credentials = getCredentials(options.env.GOOGLE_CLOUD_CREDENTIALS);
83 | } else {
84 | throw new TypeError("Missing credentials");
85 | }
86 | }
87 |
88 | let projectId = options?.projectId;
89 |
90 | if (!projectId && options?.env?.GOOGLE_CLOUD_PROJECT) {
91 | projectId = options.env.GOOGLE_CLOUD_PROJECT;
92 | }
93 |
94 | if (!projectId) {
95 | projectId = credentials.project_id;
96 | }
97 |
98 | if (!projectId) {
99 | throw new TypeError("Missing projectId");
100 | }
101 |
102 | const customToken = await createCustomToken({
103 | ...options.claims,
104 | credentials,
105 | audience:
106 | "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
107 | uid: options.uid,
108 | });
109 |
110 | const url = new URL("https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken"); // prettier-ignore
111 | url.searchParams.set("key", apiKey);
112 |
113 | const res = await fetch(url, {
114 | method: "POST",
115 | headers: { "Content-Type": "application/json" },
116 | body: JSON.stringify({
117 | token: customToken,
118 | returnSecureToken: true,
119 | }),
120 | });
121 |
122 | if (!res.ok) {
123 | const message = await res
124 | .json<{ error: { message: string } }>()
125 | .then((body) => body?.error?.message)
126 | .catch(() => undefined);
127 | throw new FetchError(message ?? "Failed to verify custom token", {
128 | response: res,
129 | });
130 | }
131 |
132 | return await res.json();
133 | }
134 |
135 | /**
136 | * Verifies the authenticity of an ID token issued by Google.
137 | *
138 | * @example
139 | * const token = await verifyIdToken({
140 | * idToken: "eyJhbGciOiJSUzI1NiIsImtpZC...yXQ"
141 | * projectId: "my-project"
142 | * waitUntil: ctx.waitUntil,
143 | * });
144 | *
145 | * @example
146 | * const token = await verifyIdToken({
147 | * idToken: "eyJhbGciOiJSUzI1NiIsImtpZC...yXQ"
148 | * waitUntil: ctx.waitUntil,
149 | * env: { GOOGLE_CLOUD_PROJECT: "my-project" }
150 | * });
151 | *
152 | * @see https://firebase.google.com/docs/auth/admin/verify-id-tokens
153 | *
154 | * @throws {TypeError} if the ID token is missing
155 | * @throws {FetchError} if unable to fetch the public key
156 | * @throws {JWTClaimValidationFailed} if the token is invalid
157 | * @throws {JWTExpired} if the token has expired
158 | */
159 | export async function verifyIdToken(options: {
160 | /**
161 | * The ID token to verify.
162 | */
163 | idToken: string;
164 | /**
165 | * Google Cloud project ID. Set to `null` to disable the check.
166 | * @default env.GOOGLE_CLOUD_PROJECT
167 | */
168 | projectId?: string | null;
169 | /**
170 | * Alternatively, you can provide the following environment variables:
171 | */
172 | env?: {
173 | /**
174 | * Google Cloud project ID.
175 | */
176 | GOOGLE_CLOUD_PROJECT?: string;
177 | /**
178 | * Google Cloud service account credentials.
179 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys
180 | */
181 | GOOGLE_CLOUD_CREDENTIALS?: string;
182 | };
183 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
184 | waitUntil?: (promise: Promise) => void;
185 | }): Promise {
186 | if (!options?.idToken) {
187 | throw new TypeError(`Missing "idToken"`);
188 | }
189 |
190 | let projectId = options?.projectId;
191 |
192 | if (projectId === undefined) {
193 | projectId = options?.env?.GOOGLE_CLOUD_PROJECT;
194 | }
195 |
196 | if (projectId === undefined && options?.env?.GOOGLE_CLOUD_CREDENTIALS) {
197 | const credentials = getCredentials(options.env.GOOGLE_CLOUD_CREDENTIALS);
198 | projectId = credentials?.project_id;
199 | }
200 |
201 | if (projectId === undefined) {
202 | throw new TypeError(`Missing "projectId"`);
203 | }
204 |
205 | if (!options.waitUntil && canUseDefaultCache) {
206 | logOnce("warn", "verifyIdToken", "Missing `waitUntil` option.");
207 | }
208 |
209 | // Import the public key from the Google Cloud project
210 | const header = decodeProtectedHeader(options.idToken);
211 | const now = Math.floor(Date.now() / 1000);
212 | const key = await importPublicKey({
213 | keyId: header.kid as string,
214 | certificateURL: "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com", // prettier-ignore
215 | waitUntil: options.waitUntil,
216 | });
217 |
218 | const { payload } = await jwtVerify(options.idToken, key, {
219 | audience: projectId == null ? undefined : projectId,
220 | issuer:
221 | projectId == null
222 | ? undefined
223 | : `https://securetoken.google.com/${projectId}`,
224 | maxTokenAge: "1h",
225 | });
226 |
227 | if (!payload.sub) {
228 | throw new errors.JWTClaimValidationFailed(`Missing "sub" claim`, "sub");
229 | }
230 |
231 | if (typeof payload.auth_time === "number" && payload.auth_time > now) {
232 | throw new errors.JWTClaimValidationFailed(
233 | `Unexpected "auth_time" claim value`,
234 | "auth_time"
235 | );
236 | }
237 |
238 | return payload as UserToken;
239 | }
240 |
241 | type VerifyCustomTokenResponse = {
242 | kind: "identitytoolkit#VerifyCustomTokenResponse";
243 | idToken: string;
244 | refreshToken: string;
245 | expiresIn: string;
246 | isNewUser: boolean;
247 | };
248 |
249 | export interface UserToken {
250 | /**
251 | * Always set to https://securetoken.google.com/GOOGLE_CLOUD_PROJECT
252 | */
253 | iss: string;
254 |
255 | /**
256 | * Always set to GOOGLE_CLOUD_PROJECT
257 | */
258 | aud: string;
259 |
260 | /**
261 | * The user's unique ID
262 | */
263 | sub: string;
264 |
265 | /**
266 | * The token issue time, in seconds since epoch
267 | */
268 | iat: number;
269 |
270 | /**
271 | * The token expiry time, normally 'iat' + 3600
272 | */
273 | exp: number;
274 |
275 | /**
276 | * The user's unique ID. Must be equal to 'sub'
277 | */
278 | user_id: string;
279 |
280 | /**
281 | * The time the user authenticated, normally 'iat'
282 | */
283 | auth_time: number;
284 |
285 | /**
286 | * The sign in provider, only set when the provider is 'anonymous'
287 | */
288 | provider_id?: "anonymous";
289 |
290 | /**
291 | * The user's primary email
292 | */
293 | email?: string;
294 |
295 | /**
296 | * The user's email verification status
297 | */
298 | email_verified?: boolean;
299 |
300 | /**
301 | * The user's primary phone number
302 | */
303 | phone_number?: string;
304 |
305 | /**
306 | * The user's display name
307 | */
308 | name?: string;
309 |
310 | /**
311 | * The user's profile photo URL
312 | */
313 | picture?: string;
314 |
315 | /**
316 | * Information on all identities linked to this user
317 | */
318 | firebase: {
319 | /**
320 | * The primary sign-in provider
321 | */
322 | sign_in_provider: SignInProvider;
323 |
324 | /**
325 | * A map of providers to the user's list of unique identifiers from
326 | * each provider
327 | */
328 | identities?: { [provider in SignInProvider]?: string[] };
329 | };
330 |
331 | /**
332 | * Custom claims set by the developer
333 | */
334 | [claim: string]: unknown;
335 |
336 | /**
337 | * @deprecated use `sub` instead
338 | */
339 | uid?: never;
340 | }
341 |
342 | export type SignInProvider =
343 | | "custom"
344 | | "email"
345 | | "password"
346 | | "phone"
347 | | "anonymous"
348 | | "google.com"
349 | | "facebook.com"
350 | | "github.com"
351 | | "twitter.com"
352 | | "microsoft.com"
353 | | "apple.com";
354 |
--------------------------------------------------------------------------------
/google/index.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | export * from "./accessToken.js";
5 | export * from "./credentials.js";
6 | export * from "./customToken.js";
7 | export * from "./idToken.js";
8 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | export * as jwt from "./core/jwt.js";
5 | export * as google from "./google/index.js";
6 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | /**
5 | * Jest configuration
6 | * https://jestjs.io/docs/configuration
7 | *
8 | * @type {import("@jest/types").Config.InitialOptions}
9 | */
10 | export default {
11 | testEnvironment: "miniflare",
12 |
13 | testPathIgnorePatterns: [
14 | "/.git/",
15 | "/.yarn/",
16 | "/dist/",
17 | ],
18 |
19 | moduleFileExtensions: [
20 | "ts",
21 | "js",
22 | "mjs",
23 | "cjs",
24 | "jsx",
25 | "ts",
26 | "tsx",
27 | "json",
28 | "node",
29 | ],
30 |
31 | modulePathIgnorePatterns: ["/dist/"],
32 |
33 | setupFiles: ["/test/setup.ts"],
34 |
35 | transform: {
36 | "\\.ts$": "babel-jest",
37 | },
38 |
39 | extensionsToTreatAsEsm: [".ts"],
40 | };
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web-auth-library",
3 | "version": "1.0.3",
4 | "packageManager": "yarn@4.0.0-rc.39",
5 | "description": "Authentication library for the browser environment using Web Crypto API",
6 | "license": "MIT",
7 | "author": {
8 | "name": "Kriasoft",
9 | "email": "hello@kriasoft.com",
10 | "url": "https://github.com/kriasoft"
11 | },
12 | "contributors": [
13 | {
14 | "name": "Konstantin Tarkus",
15 | "email": "hello@tarkus.me",
16 | "url": "https://github.com/koistya"
17 | }
18 | ],
19 | "funding": [
20 | {
21 | "type": "github",
22 | "url": "https://github.com/sponsors/kriasoft"
23 | },
24 | {
25 | "type": "patreon",
26 | "url": "https://www.patreon.com/koistya"
27 | }
28 | ],
29 | "repository": "github:kriasoft/web-auth-library",
30 | "keywords": [
31 | "auth",
32 | "authentication",
33 | "authorization",
34 | "bearer",
35 | "browser",
36 | "bun",
37 | "cloudflare-workers",
38 | "cloudflare",
39 | "crypto",
40 | "decrypt",
41 | "deno",
42 | "encrypt",
43 | "hono",
44 | "jsonwebtoken",
45 | "jwk",
46 | "jwt",
47 | "keys",
48 | "oauth",
49 | "oauth2",
50 | "sign",
51 | "subtlecrypto",
52 | "token",
53 | "typescript",
54 | "web",
55 | "webcrypto"
56 | ],
57 | "files": [
58 | "dist"
59 | ],
60 | "type": "module",
61 | "exports": {
62 | ".": "./dist/index.js",
63 | "./jwt": "./dist/core/jwt.js",
64 | "./google": {
65 | "types": "./dist/google/index.d.ts",
66 | "import": "./dist/google/index.js",
67 | "default": "./dist/google/index.js"
68 | },
69 | "./package.json": "./package.json"
70 | },
71 | "scripts": {
72 | "lint": "eslint --report-unused-disable-directives .",
73 | "test": "node --experimental-vm-modules $(yarn bin jest)",
74 | "build": "rm -rf ./dist && yarn tsc"
75 | },
76 | "dependencies": {
77 | "jose": ">= 4.12.0 < 5.0.0",
78 | "rfc4648": "^1.5.2"
79 | },
80 | "devDependencies": {
81 | "@babel/cli": "^7.21.0",
82 | "@babel/core": "^7.21.0",
83 | "@babel/preset-env": "^7.20.2",
84 | "@babel/preset-typescript": "^7.21.0",
85 | "@cloudflare/workers-types": "^4.20230228.0",
86 | "@types/jest": "^29.4.0",
87 | "@typescript-eslint/eslint-plugin": "^5.54.0",
88 | "@typescript-eslint/parser": "^5.54.0",
89 | "babel-jest": "^29.4.3",
90 | "babel-plugin-replace-import-extension": "^1.1.3",
91 | "dotenv": "^16.0.3",
92 | "envalid": "^7.3.1",
93 | "eslint": "^8.35.0",
94 | "jest": "^29.4.3",
95 | "jest-environment-miniflare": "^2.12.1",
96 | "prettier": "^2.8.4",
97 | "typescript": "^4.9.5"
98 | },
99 | "babel": {
100 | "presets": [
101 | [
102 | "@babel/preset-env",
103 | {
104 | "targets": "last 2 Chrome versions",
105 | "modules": false
106 | }
107 | ],
108 | "@babel/preset-typescript"
109 | ],
110 | "plugins": [
111 | [
112 | "replace-import-extension",
113 | {
114 | "extMapping": {
115 | ".js": ".ts"
116 | }
117 | }
118 | ]
119 | ]
120 | },
121 | "eslintConfig": {
122 | "root": true,
123 | "env": {
124 | "browser": true
125 | },
126 | "parser": "@typescript-eslint/parser",
127 | "plugins": [
128 | "@typescript-eslint"
129 | ],
130 | "extends": [
131 | "eslint:recommended",
132 | "plugin:@typescript-eslint/recommended"
133 | ],
134 | "overrides": [
135 | {
136 | "files": [
137 | "*.test.ts",
138 | "*.test.js"
139 | ],
140 | "env": {
141 | "jest": true
142 | }
143 | }
144 | ],
145 | "ignorePatterns": [
146 | "/.yarn/**",
147 | "/dist/**",
148 | "/node_modules/**"
149 | ]
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/test/env.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | import { cleanEnv, str } from "envalid";
5 |
6 | export default cleanEnv(process.env, {
7 | GOOGLE_CLOUD_PROJECT: str(),
8 | GOOGLE_CLOUD_CREDENTIALS: str(),
9 | FIREBASE_API_KEY: str(),
10 | });
11 |
--------------------------------------------------------------------------------
/test/setup.ts:
--------------------------------------------------------------------------------
1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */
2 | /* SPDX-License-Identifier: MIT */
3 |
4 | import dotenv from "dotenv";
5 |
6 | dotenv.config({ path: "./test/test.override.env" });
7 | dotenv.config({ path: "./test/test.env" });
8 |
--------------------------------------------------------------------------------
/test/test.env:
--------------------------------------------------------------------------------
1 | # Environment variables to be used for testing
2 | # NOTE: You can override them locally in test.override.env file
3 |
4 | # Google Cloud
5 | # https://cloud.google.com/iam/docs/creating-managing-service-account-keys
6 | GOOGLE_CLOUD_PROJECT=kriasoft
7 | GOOGLE_CLOUD_CREDENTIALS={"type":"service_account","project_id":"example","private_key_id":"25809e59963e2cbbe616cc9dd2feedcc4b620da5","private_key":"-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC407XFlKSwvNbQ\nVnmgZK4zkUOgGARwXpdvTZXvPwCaM1jA+g6WxAv5sIqj9JVEQnwBmre+S5uqJ7tD\nfLlMei7Vbb1FuVLlWfYu6STAkuzU5JC13b+AwRqYDNNtIeNngl9FEnfofHmoCaox\n8r4UAALr2/R3KPoUUGq5Reb70XUwuw2cyLDUiQEwUoLm3HUcIbHFAwIJ/W+s9ghy\n9FBBPAYVUMLudutlb8yIna0qW84rreqBgSGFCmU/R/zeNcNbWIasWBkqgjV661ci\nqXViBuOC9BWeJ0cGIa2JV/IeHrjqROsWo8h2ev1NWjZ7s1LP33ylgGInMgxuDvRI\nfUh8z7nHAgMBAAECggEAMSLfjUKCMhZSCZsjxJdflIgG8XXRIEqVedqnhK48K8KA\n0vTnkf9Wq6/ae9IXKMmADDEkriuNm8PqTfvHi2RkNQtyqSmmtyCeiUQkKConWkXV\njvP/6Gvt9QRb5QSAX1FSoJtTU3RcJ2dCXvsIu2pxXGDichdrvKDQbqb9zG6X+Dce\nmO7lu/xBStR/Q4aD2nC6TF799gSPR3yI+XyfHdBzBaN35RqVfIONByy4VH2ArIYt\npxXdsevDR4HYxV7hciSIehXTDL0x9+zUXRzFslUGY/E1c2fTSJOb7IwAEyeMzB0t\n/6i+aIZrMdVPcwxmVVG90Y0CZE+OGoQq1nm6DjWdqQKBgQDjAFiKFYP4KXijPzfm\n/1Idvht5Ol2Tr2+BQ7trkdkLbLrxcmIyUzVlBMU82uQVhTqCXZAy8eYuzsXMHp9J\nibgg4YqtdX+wIYaO8tELhAEtO3tr/nMsVe8su5RQlhiCTCO4HR0OyOMhnnudYwUn\ncO+rbX3cixOMxADMS/FTWQ/r7QKBgQDQcCF5WmpThSg3boxVy0jJVSB6/8+BTZdy\nqF+wcb2BqJe8OtnHnbXZTakau+f13Jy4ttJYB/moe/trjCbl8ihcDmMGbuAg3Eir\nax2mm4ZetUPVrgZ+Y/wwjA6lO1THWdI8cIQJE4mNEhXeqd3FFK0X445FHdiroBbd\nNAji4H0OAwKBgA9vLpX08IwnBbTTz5E9OvAaxPNxLHumKga3/D5MJF3Kfst744FY\ndwDvWhnRKEDuVhQXGH7eQ7BbDsfaLSpq2sIhk7RHkO8A2I1PpTcLOqlAqhulqV8S\nWLjJ6EOycOgrFSKnmBoxPoBCrlT9LpSH8UPOpgggzKt9iDBb2YS5QYPhAoGAVmJy\nbRHcyRqBjV+ih5gFZXODT5afUC5xGtLPPZgV+xt9L0SQp1skV5gJAoxn2QyCY0dZ\nq6Q6gupHS848/MW8llJcFflzqArDj0+IbVk9ehjTsUY7aLxVc2VIWJBbVXdTWzsi\nbYSMWEvrhmmOALTN+/2SI/D3sEFb2HdNS4HQMjMCgYBuEBqRQNMofnzDgrKN8AwQ\n4/SAVq4ER1lqs4QTixIvFkfeGAH/AzcfsvwYjzi6Pwu8TMxvrmQhrDyvjsrfB204\nU+JhsYJje/wBTWucVEuaWZ/H5gUiFnMV8SytmvfMszRhWX+sG3ucQOLC4J9Enbh5\nsDSQTShw4U1Wd03w55TrZg==\n-----END PRIVATE KEY-----\n","client_email":"example@example.iam.gserviceaccount.com","client_id":"118360562778253889493","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/example%40example.iam.gserviceaccount.com"}
8 | FIREBASE_API_KEY=xxxxx
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | "lib": ["ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "ESNext", /* Specify what module code is generated. */
29 | "rootDir": "./", /* Specify the root folder within your source files. */
30 | "moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | "types": ["@cloudflare/workers-types", "jest"], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "resolveJsonModule": true, /* Enable importing .json files. */
39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
40 |
41 | /* JavaScript Support */
42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
45 |
46 | /* Emit */
47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
52 | "outDir": "./dist", /* Specify an output folder for all emitted files. */
53 | // "removeComments": true, /* Disable emitting comments. */
54 | // "noEmit": true, /* Disable emitting files from a compilation. */
55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63 | // "newLine": "crlf", /* Set the newline character for emitting files. */
64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70 |
71 | /* Interop Constraints */
72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
77 |
78 | /* Type Checking */
79 | "strict": true, /* Enable all strict type-checking options. */
80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
98 |
99 | /* Completeness */
100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
102 | }
103 | }
104 |
--------------------------------------------------------------------------------