Full Code of sst/openauth for AI

master 98dc59625e65 cached
237 files
577.6 KB
164.3k tokens
279 symbols
1 requests
Download .txt
Showing preview only (637K chars total). Download the full file or copy to clipboard to get everything.
Repository: sst/openauth
Branch: master
Commit: 98dc59625e65
Files: 237
Total size: 577.6 KB

Directory structure:
gitextract_6_z9n4fm/

├── .changeset/
│   ├── README.md
│   ├── commit.cjs
│   ├── config.json
│   ├── popular-geese-reply.md
│   ├── stupid-boats-play.md
│   └── ten-pans-invent.md
├── .github/
│   ├── CODE_OF_CONDUCT
│   └── workflows/
│       ├── docs.yml
│       ├── format.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .prettierrc
├── CNAME
├── LICENSE
├── README.md
├── bun.lockb
├── bunfig.toml
├── examples/
│   ├── .gitignore
│   ├── README.md
│   ├── client/
│   │   ├── astro/
│   │   │   ├── .gitignore
│   │   │   ├── .vscode/
│   │   │   │   ├── extensions.json
│   │   │   │   └── launch.json
│   │   │   ├── README.md
│   │   │   ├── astro.config.mjs
│   │   │   ├── package.json
│   │   │   ├── src/
│   │   │   │   ├── auth.ts
│   │   │   │   ├── components/
│   │   │   │   │   └── Welcome.astro
│   │   │   │   ├── env.d.ts
│   │   │   │   ├── layouts/
│   │   │   │   │   └── Layout.astro
│   │   │   │   ├── middleware.ts
│   │   │   │   └── pages/
│   │   │   │       ├── callback.ts
│   │   │   │       └── index.astro
│   │   │   └── tsconfig.json
│   │   ├── cloudflare-api/
│   │   │   ├── api.ts
│   │   │   └── package.json
│   │   ├── jwt-api/
│   │   │   ├── CHANGELOG.md
│   │   │   ├── README.md
│   │   │   ├── index.ts
│   │   │   └── package.json
│   │   ├── lambda-api/
│   │   │   ├── api.ts
│   │   │   └── package.json
│   │   ├── nextjs/
│   │   │   ├── .gitignore
│   │   │   ├── CHANGELOG.md
│   │   │   ├── README.md
│   │   │   ├── app/
│   │   │   │   ├── actions.ts
│   │   │   │   ├── api/
│   │   │   │   │   └── callback/
│   │   │   │   │       └── route.ts
│   │   │   │   ├── auth.ts
│   │   │   │   ├── globals.css
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── page.module.css
│   │   │   │   └── page.tsx
│   │   │   ├── next.config.ts
│   │   │   ├── package.json
│   │   │   └── tsconfig.json
│   │   ├── react/
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src/
│   │   │   │   ├── App.tsx
│   │   │   │   ├── AuthContext.tsx
│   │   │   │   ├── main.tsx
│   │   │   │   └── vite-env.d.ts
│   │   │   ├── tsconfig.app.json
│   │   │   ├── tsconfig.json
│   │   │   ├── tsconfig.node.json
│   │   │   └── vite.config.ts
│   │   └── sveltekit/
│   │       ├── .npmrc
│   │       ├── package.json
│   │       ├── src/
│   │       │   ├── app.d.ts
│   │       │   ├── app.html
│   │       │   ├── hooks.server.ts
│   │       │   ├── lib/
│   │       │   │   └── auth.server.ts
│   │       │   └── routes/
│   │       │       ├── +page.server.ts
│   │       │       ├── +page.svelte
│   │       │       └── callback/
│   │       │           └── +server.ts
│   │       ├── svelte.config.js
│   │       ├── tsconfig.json
│   │       └── vite.config.ts
│   ├── issuer/
│   │   ├── bun/
│   │   │   ├── .gitignore
│   │   │   ├── issuer.ts
│   │   │   └── package.json
│   │   ├── cloudflare/
│   │   │   ├── issuer.ts
│   │   │   ├── package.json
│   │   │   ├── sst-env.d.ts
│   │   │   └── sst.config.ts
│   │   ├── custom-frontend/
│   │   │   ├── auth/
│   │   │   │   ├── issuer.ts
│   │   │   │   └── package.json
│   │   │   ├── frontend/
│   │   │   │   ├── frontend.tsx
│   │   │   │   └── package.json
│   │   │   └── package.json
│   │   ├── lambda/
│   │   │   ├── issuer.ts
│   │   │   ├── package.json
│   │   │   ├── sst-env.d.ts
│   │   │   └── sst.config.ts
│   │   └── node/
│   │       ├── .gitignore
│   │       ├── authorizer.ts
│   │       └── package.json
│   ├── quickstart/
│   │   ├── sst/
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   ├── app/
│   │   │   │   ├── actions.ts
│   │   │   │   ├── api/
│   │   │   │   │   └── callback/
│   │   │   │   │       └── route.ts
│   │   │   │   ├── auth.ts
│   │   │   │   ├── globals.css
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── page.module.css
│   │   │   │   └── page.tsx
│   │   │   ├── auth/
│   │   │   │   ├── index.ts
│   │   │   │   └── subjects.ts
│   │   │   ├── next.config.ts
│   │   │   ├── package.json
│   │   │   ├── sst-env.d.ts
│   │   │   ├── sst.config.ts
│   │   │   └── tsconfig.json
│   │   └── standalone/
│   │       ├── .gitignore
│   │       ├── README.md
│   │       ├── app/
│   │       │   ├── actions.ts
│   │       │   ├── api/
│   │       │   │   └── callback/
│   │       │   │       └── route.ts
│   │       │   ├── auth.ts
│   │       │   ├── globals.css
│   │       │   ├── layout.tsx
│   │       │   ├── page.module.css
│   │       │   └── page.tsx
│   │       ├── auth/
│   │       │   ├── index.ts
│   │       │   └── subjects.ts
│   │       ├── bun.lockb
│   │       ├── next.config.ts
│   │       ├── package.json
│   │       └── tsconfig.json
│   ├── subjects.ts
│   └── tsconfig.json
├── package.json
├── packages/
│   └── openauth/
│       ├── CHANGELOG.md
│       ├── bunfig.toml
│       ├── package.json
│       ├── script/
│       │   └── build.ts
│       ├── src/
│       │   ├── client.ts
│       │   ├── css.d.ts
│       │   ├── error.ts
│       │   ├── index.ts
│       │   ├── issuer.ts
│       │   ├── jwt.ts
│       │   ├── keys.ts
│       │   ├── pkce.ts
│       │   ├── provider/
│       │   │   ├── apple.ts
│       │   │   ├── arctic.ts
│       │   │   ├── code.ts
│       │   │   ├── cognito.ts
│       │   │   ├── discord.ts
│       │   │   ├── facebook.ts
│       │   │   ├── github.ts
│       │   │   ├── google.ts
│       │   │   ├── index.ts
│       │   │   ├── jumpcloud.ts
│       │   │   ├── keycloak.ts
│       │   │   ├── linkedin.ts
│       │   │   ├── microsoft.ts
│       │   │   ├── oauth2.ts
│       │   │   ├── oidc.ts
│       │   │   ├── password.ts
│       │   │   ├── provider.ts
│       │   │   ├── slack.ts
│       │   │   ├── spotify.ts
│       │   │   ├── twitch.ts
│       │   │   ├── x.ts
│       │   │   └── yahoo.ts
│       │   ├── random.ts
│       │   ├── storage/
│       │   │   ├── aws.ts
│       │   │   ├── cloudflare.ts
│       │   │   ├── dynamo.ts
│       │   │   ├── memory.ts
│       │   │   └── storage.ts
│       │   ├── subject.ts
│       │   ├── ui/
│       │   │   ├── base.tsx
│       │   │   ├── code.tsx
│       │   │   ├── form.tsx
│       │   │   ├── icon.tsx
│       │   │   ├── password.tsx
│       │   │   ├── select.tsx
│       │   │   ├── theme.ts
│       │   │   └── ui.css
│       │   └── util.ts
│       ├── test/
│       │   ├── client.test.ts
│       │   ├── issuer.test.ts
│       │   ├── scrap.test.ts
│       │   ├── storage.test.ts
│       │   └── util.test.ts
│       └── tsconfig.json
├── scripts/
│   └── format
└── www/
    ├── .gitignore
    ├── .vscode/
    │   ├── extensions.json
    │   └── launch.json
    ├── README.md
    ├── astro.config.mjs
    ├── bun.lockb
    ├── config.ts
    ├── generate.ts
    ├── package.json
    ├── src/
    │   ├── components/
    │   │   ├── Hero.astro
    │   │   └── Lander.astro
    │   ├── content/
    │   │   ├── config.ts
    │   │   └── docs/
    │   │       ├── docs/
    │   │       │   ├── client.mdx
    │   │       │   ├── index.mdx
    │   │       │   ├── issuer.mdx
    │   │       │   ├── provider/
    │   │       │   │   ├── apple.mdx
    │   │       │   │   ├── code.mdx
    │   │       │   │   ├── cognito.mdx
    │   │       │   │   ├── discord.mdx
    │   │       │   │   ├── facebook.mdx
    │   │       │   │   ├── github.mdx
    │   │       │   │   ├── google.mdx
    │   │       │   │   ├── jumpcloud.mdx
    │   │       │   │   ├── keycloak.mdx
    │   │       │   │   ├── microsoft.mdx
    │   │       │   │   ├── oauth2.mdx
    │   │       │   │   ├── oidc.mdx
    │   │       │   │   ├── password.mdx
    │   │       │   │   ├── slack.mdx
    │   │       │   │   ├── spotify.mdx
    │   │       │   │   ├── twitch.mdx
    │   │       │   │   ├── x.mdx
    │   │       │   │   └── yahoo.mdx
    │   │       │   ├── start/
    │   │       │   │   ├── sst.mdx
    │   │       │   │   └── standalone.mdx
    │   │       │   ├── storage/
    │   │       │   │   ├── cloudflare.mdx
    │   │       │   │   ├── dynamo.mdx
    │   │       │   │   └── memory.mdx
    │   │       │   ├── subject.mdx
    │   │       │   └── ui/
    │   │       │       ├── code.mdx
    │   │       │       ├── password.mdx
    │   │       │       ├── select.mdx
    │   │       │       └── theme.mdx
    │   │       └── index.mdx
    │   ├── custom.css
    │   ├── env.d.ts
    │   └── styles/
    │       └── lander.css
    └── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .changeset/README.md
================================================
# Changesets

Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)

We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)


================================================
FILE: .changeset/commit.cjs
================================================
/** @type {import('@changesets/types').CommitFunctions["getAddMessage"]} */
module.exports.getAddMessage = async (changeset) => {
  return changeset.summary;
};


================================================
FILE: .changeset/config.json
================================================
{
  "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": "./commit.cjs",
  "fixed": [["@openauthjs/openauth"]],
  "linked": [],
  "access": "public",
  "baseBranch": "master",
  "updateInternalDependencies": "patch",
  "ignore": ["@openauthjs/example-*"]
}


================================================
FILE: .changeset/popular-geese-reply.md
================================================
---
"@openauthjs/openauth": patch
---

update google icon to comply with branding guidelines


================================================
FILE: .changeset/stupid-boats-play.md
================================================
---
"@openauthjs/openauth": patch
---

allow auth style autodetection


================================================
FILE: .changeset/ten-pans-invent.md
================================================
---
"@openauthjs/openauth": patch
---

add linkedin adapter


================================================
FILE: .github/CODE_OF_CONDUCT
================================================
# Code of Conduct

I don't typically set up a code of conduct for our projects but given this one is security related it will draw a very specific set of problems I want to avoid. There's only two rules

1. Reporting security issues

If you find a security issue please report them to me directly on [X](https://twitter.com/thdxr) or [Bluesky](https://bsky.app/). Do not open a public issue or post publicly in case the issue can be exploited. Feel free to give us a window of time to respond before disclosing it publicly - that seems fair.

2. Reporting "security" issues

A lot of things that seem to fall in that first category are not really security problems, just tradeoffs that were made in the design of OpenAuth. Security products attract a lot of binary opinions like "never use X". We reject this type of thinking entirely - security is a spectrum of usability and infinitely optimizing for "security" does not yield a good product.

All discussions around the tradeoffs that were made must consider this - if you disagree with a decision you MUST articulate why the decision was probably made before you argue against it. Eg. "X seem to be used because of benefit [a] and their downside [b] is mitigated by [c] BUT I do not think this is enough because of [d]"

We do not tolerate wasting the maintainers time and forcing them to articulate this nuance. If something is not clear of course you can ask for clarification.


================================================
FILE: .github/workflows/docs.yml
================================================
name: docs

on:
  # Trigger the workflow every time you push to the `main` branch
  # Using a different branch name? Replace `main` with your branch’s name
  push:
    branches: [master]
  # Allows you to run this workflow manually from the Actions tab on GitHub.
  workflow_dispatch:

# Allow this job to clone the repo and create a page deployment
permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout your repository using git
        uses: actions/checkout@v4
      - name: Install, build, and upload your site
        uses: withastro/action@v3
        with:
          path: www

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4
        with:
          path: www


================================================
FILE: .github/workflows/format.yml
================================================
name: format

on:
  push:
    branches: [master]
  pull_request:
  workflow_dispatch:

jobs:
  format:
    runs-on: ubuntu-latest

    permissions:
      contents: write
      pull-requests: write

    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.head_ref }}
          fetch-depth: 0
      - uses: oven-sh/setup-bun@v2
      - run: |
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          ./scripts/format


================================================
FILE: .github/workflows/release.yml
================================================
name: release

on:
  push:
    branches:
      - master

permissions:
  contents: write
  pull-requests: write

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
  release:
    name: release
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: oven-sh/setup-bun@v2
      - run: bun install
      - id: changesets
        uses: changesets/action@v1
        with:
          publish: bun run release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}


================================================
FILE: .github/workflows/test.yml
================================================
name: test

on:
  push:
    branches: [master]
  pull_request:
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest
      - run: bun install
      - run: cd packages/openauth && bun run build
      - run: cd packages/openauth && bun test


================================================
FILE: .gitignore
================================================
/node_modules
.sst
.env
dist
persist.json
.DS_Store
notes
.nvim.lua
.svelte-kit

================================================
FILE: .prettierrc
================================================
{
  "semi": false,
}


================================================
FILE: CNAME
================================================
openauth.js.org


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2024 SST

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



================================================
FILE: README.md
================================================
<p align="center">
  <a href="https://openauth.js.org">
    <picture>
      <source srcset="https://raw.githubusercontent.com/toolbeam/identity/main/openauth/logo-dark.svg" media="(prefers-color-scheme: dark)">
      <source srcset="https://raw.githubusercontent.com/toolbeam/identity/main/openauth/logo-light.svg" media="(prefers-color-scheme: light)">
      <img src="https://raw.githubusercontent.com/toolbeam/identity/main/openauth/logo-light.svg" alt="OpenAuth logo">
    </picture>
  </a>
</p>
<p align="center">
  <a href="https://sst.dev/discord"><img alt="Discord" src="https://img.shields.io/discord/983865673656705025?style=flat-square&label=Discord" /></a>
  <a href="https://www.npmjs.com/package/@openauthjs/openauth"><img alt="npm" src="https://img.shields.io/npm/v/%40openauthjs%2Fcore?style=flat-square" /></a>
  <a href="https://github.com/toolbeam/openauth/actions/workflows/release.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/toolbeam/openauth/release.yml?style=flat-square&branch=master" /></a>
</p>

---

[OpenAuth](https://openauth.js.org) is a standards-based auth provider for web apps, mobile apps, single pages apps, APIs, or 3rd party clients. It is currently in beta.

- **Universal**: You can deploy it as a standalone service or embed it into an existing application. It works with any framework or platform.
- **Self-hosted**: It runs entirely on your infrastructure and can be deployed on Node.js, Bun, AWS Lambda, or Cloudflare Workers.
- **Standards-based**: It implements the OAuth 2.0 spec and is based on web standards. So any OAuth client can use it.
- **Customizable**: It comes with prebuilt themeable UI that you can customize or opt out of.

<picture>
  <source srcset="https://raw.githubusercontent.com/toolbeam/identity/main/openauth/assets/themes-dark.png" media="(prefers-color-scheme: dark)">
  <source srcset="https://raw.githubusercontent.com/toolbeam/identity/main/openauth/assets/themes-light.png" media="(prefers-color-scheme: dark)">
  <img src="https://raw.githubusercontent.com/toolbeam/identity/main/openauth/assets/themes-light.png" alt="OpenAuth themes">
</picture>

## Quick Start

If you just want to get started as fast as possible you can jump straight into the [code examples](https://github.com/toolbeam/openauth/tree/master/examples) folder and copy paste away. There are also [SST components](https://sst.dev/docs/component/aws/auth) for deploying everything OpenAuth needs.

## Approach

While there are many open source solutions for auth, almost all of them are libraries that are meant to be embedded into a single application. Centralized auth servers typically are delivered as SaaS services - eg Auth0 or Clerk.

OpenAuth instead is a centralized auth server that runs on your own infrastructure and has been designed for ease of self hosting. It can be used to authenticate all of your applications - web apps, mobile apps, internal admin tools, etc.

It adheres mostly to OAuth 2.0 specifications - which means anything that can speak OAuth can use it to receive access and refresh tokens. When a client initiates an authorization flow, OpenAuth will hand off to one of the configured providers - this can be third party identity providers like Google, GitHub, etc or built in flows like email/password or pin code.

Because it follows these specifications it can even be used to issue credentials for third party applications - allowing you to implement "login with myapp" flows.

OpenAuth very intentionally does not attempt to solve user management. We've found that this is a very difficult problem given the wide range of databases and drivers that are used in the JS ecosystem. Additionally it's quite hard to build data abstractions that work for every use case. Instead, once a user has identified themselves OpenAuth will invoke a callback where you can implement your own user lookup/creation logic.

While OpenAuth tries to be mostly stateless, it does need to store a minimal amount of data (refresh tokens, password hashes, etc). However this has been reduced to a simple KV store with various implementations for zero overhead systems like Cloudflare KV and DynamoDB. You should never need to directly access any data that is stored in there.

There is also a themeable UI that you can use to get going without implementing any designs yourself. This is built on top of a lower level system so you can copy paste the default UI and tweak it or opt out entirely and implement your own.

Finally, OpenAuth is created by the maintainers of [SST](https://sst.dev) which is a tool to manage all the infrastructure for your app. It contains components for OpenAuth that make deploying it to AWS or Cloudflare as simple as it can get.

## Tutorial

We'll show how to deploy the auth server and then a sample app that uses it.

### Auth server

Start by importing the `issuer` function from the `@openauthjs/openauth` package.

```ts
import { issuer } from "@openauthjs/openauth"
```

OpenAuth is built on top of [Hono](https://github.com/honojs/hono) which is a minimal web framework that can run anywhere. The `issuer` function creates a Hono app with all of the auth server implemented that you can then deploy to AWS Lambda, Cloudflare Workers, or in a container running under Node.js or Bun.

The `issuer` function requires a few things:

```ts
const app = issuer({
  providers: { ... },
  storage,
  subjects,
  success: async (ctx, value) => { ... }
})
```

First we need to define some providers that are enabled - these are either third party identity providers like Google, GitHub, etc or built in flows like email/password or pin code. You can also implement your own. Let's try the GitHub provider.

```ts
import { GithubProvider } from "@openauthjs/openauth/provider/github"

const app = issuer({
  providers: {
    github: GithubProvider({
      clientID: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
      scopes: ["user:email"],
    }),
  },
  ...
})
```

Providers take some configuration - since this is a third party identity provider there is no UI to worry about and all it needs is a client ID, secret and some scopes. Let's add the password provider which is a bit more complicated.

```ts
import { PasswordProvider } from "@openauthjs/openauth/provider/password"

const app = issuer({
  providers: {
    github: ...,
    password: PasswordProvider(...),
  },
  ...
})
```

The password provider is quite complicated as username/password involve a lot of flows so there are a lot of callbacks to implement. However you can opt into the default UI which has all of this already implemented for you. The only thing you have to specify is how to send a code for forgot password/email verification. In this case we'll log the code but you would send this over email.

```ts
import { PasswordProvider } from "@openauthjs/openauth/provider/password"
import { PasswordUI } from "@openauthjs/openauth/ui/password"

const app = issuer({
  providers: {
    github: ...,
    password: PasswordProvider(
      PasswordUI({
        sendCode: async (email, code) => {
          console.log(email, code)
        },
      }),
    ),
  },
  ...
})
```

Next up is the `subjects` field. Subjects are what the access token generated at the end of the auth flow will map to. Under the hood, the access token is a JWT that contains this data. You will likely just have a single subject to start but you can define additional ones for different types of users.

```ts
import { object, string } from "valibot"

const subjects = createSubjects({
  user: object({
    userID: string(),
    // may want to add workspaceID here if doing a multi-tenant app
    workspaceID: string(),
  }),
})
```

Note we are using [valibot](https://github.com/fabian-hiller/valibot) to define the shape of the subject so it can be validated properly. You can use any validation library that is following the [standard-schema specification](https://github.com/standard-schema/standard-schema) - the next version of Zod will support this.

You typically will want to place subjects in its own file as it can be imported by all of your apps. You can pass it to the issuer in the `subjects` field.

```ts
import { subjects } from "./subjects.js"

const app = issuer({
  providers: { ... },
  subjects,
  ...
})
```

Next we'll implement the `success` callback which receives the payload when a user successfully completes a provider flow.

```ts
const app = issuer({
  providers: { ... },
  subjects,
  async success(ctx, value) {
    let userID
    if (value.provider === "password") {
      console.log(value.email)
      userID = ... // lookup user or create them
    }
    if (value.provider === "github") {
      console.log(value.tokenset.access)
      userID = ... // lookup user or create them
    }
    return ctx.subject("user", {
      userID,
      'a workspace id'
    })
  }
})
```

Note all of this is typesafe - based on the configured providers you will receive different properties in the `value` object. Also the `subject` method will only accept properties. Note - most callbacks in OpenAuth can return a `Response` object. In this case if something goes wrong, you can return a `Response.redirect("...")` sending them to a different place or rendering an error.

Next we have the `storage` field which defines where things like refresh tokens and password hashes are stored. If on AWS we recommend DynamoDB, if on Cloudflare we recommend Cloudflare KV. We also have a MemoryStore used for testing.

```ts
import { MemoryStorage } from "@openauthjs/openauth/storage/memory"

const app = issuer({
  providers: { ... },
  subjects,
  async success(ctx, value) { ... },
  storage: MemoryStorage(),
})
```

And now we are ready to deploy! Here's how you do that depending on your infrastructure.

```ts
// Bun
export default app

// Cloudflare
export default app

// Lambda
import { handle } from "hono/aws-lambda"
export const handler = handle(app)

// Node.js
import { serve } from "@hono/node-server"
serve(app)
```

You now have a centralized auth server. Test it out by visiting `/.well-known/oauth-authorization-server` - you can see a live example [here](https://auth.terminal.shop/.well-known/oauth-authorization-server).

### Auth client

Since this is a standard OAuth server you can use any libraries for OAuth and it will work. OpenAuth does provide some light tooling for this although even a manual flow is pretty simple. You can create a client like this:

```ts
import { createClient } from "@openauthjs/openauth/client"

const client = createClient({
  clientID: "my-client",
  issuer: "https://auth.myserver.com", // url to the OpenAuth server
})
```

#### SSR Sites

If your frontend has a server component you can use the code flow. Redirect the user here

```ts
const { url } = await client.authorize(
  <redirect-uri>,
  "code"
)
```

You can make up a `client_id` that represents your app. This will initiate the auth flow and user will be redirected to the `redirect_uri` you provided with a query parameter `code` which you can exchange for an access token.

```ts
// the redirect_uri is the original redirect_uri you passed in and is used for verification
const tokens = await client.exchange(query.get("code"), redirect_uri)
console.log(tokens.access, tokens.refresh)
```

You likely want to store both the access token and refresh token in an HTTP only cookie so they are sent up with future requests. Then you can use the `client` to verify the tokens.

```ts
const verified = await client.verify(subjects, cookies.get("access_token")!, {
  refresh: cookies.get("refresh_token") || undefined,
})
console.log(
  verified.subject.type,
  verified.subject.properties,
  verified.refresh,
  verified.access,
)
```

Passing in the refresh token is optional but if you do, this function will automatically refresh the access token if it has expired. It will return a new access token and refresh token which you should set back into the cookies.

#### SPA Sites, Mobile apps, etc

In cases where you do not have a server, you can use the `token` flow with `pkce` on the frontend.

```ts
const { challenge, url } = await client.authorize(<redirect_uri>, "code", { pkce: true })
localStorage.setItem("challenge", JSON.stringify(challenge))
location.href = url
```

When the auth flow is complete the user's browser will be redirected to the `redirect_uri` with a `code` query parameter. You can then exchange the code for access/refresh tokens.

```ts
const challenge = JSON.parse(localStorage.getItem("challenge"))
const exchanged = await client.exchange(
  query.get("code"),
  redirect_uri,
  challenge.verifier,
)
if (exchanged.err) throw new Error("Invalid code")
localStorage.setItem("access_token", exchanged.tokens.access)
localStorage.setItem("refresh_token", exchanged.tokens.refresh)
```

Then when you make requests to your API you can include the access token in the `Authorization` header.

```ts
const accessToken = localStorage.getItem("access_token")
fetch("https://auth.example.com/api/user", {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
})
```

And then you can verify the access token on the server.

```ts
const verified = await client.verify(subjects, accessToken)
console.log(verified.subject)
```

---

OpenAuth is created by the maintainers of [SST](https://sst.dev).

**Join our community** [Discord](https://sst.dev/discord) | [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)


================================================
FILE: bunfig.toml
================================================
[install]
exact = true


================================================
FILE: examples/.gitignore
================================================
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore

# Logs

logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Caches

.cache

# Diagnostic reports (https://nodejs.org/api/report.html)

report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json

# Runtime data

pids
_.pid
_.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover

lib-cov

# Coverage directory used by tools like istanbul

coverage
*.lcov

# nyc test coverage

.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)

.grunt

# Bower dependency directory (https://bower.io/)

bower_components

# node-waf configuration

.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)

build/Release

# Dependency directories

node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)

web_modules/

# TypeScript cache

*.tsbuildinfo

# Optional npm cache directory

.npm

# Optional eslint cache

.eslintcache

# Optional stylelint cache

.stylelintcache

# Microbundle cache

.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history

.node_repl_history

# Output of 'npm pack'

*.tgz

# Yarn Integrity file

.yarn-integrity

# dotenv environment variable files

.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)

.parcel-cache

# Next.js build output

.next
out

# Nuxt.js build / generate output

.nuxt
dist

# Gatsby files

# Comment in the public line in if your project uses Gatsby and not Next.js

# https://nextjs.org/blog/next-9-1#public-directory-support

# public

# vuepress build output

.vuepress/dist

# vuepress v2.x temp and cache directory

.temp

# Docusaurus cache and generated files

.docusaurus

# Serverless directories

.serverless/

# FuseBox cache

.fusebox/

# DynamoDB Local files

.dynamodb/

# TernJS port file

.tern-port

# Stores VSCode versions used for testing VSCode extensions

.vscode-test

# yarn v2

.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# IntelliJ based IDEs
.idea

# Finder (MacOS) folder config
.DS_Store

# sst
.sst


================================================
FILE: examples/README.md
================================================
# Examples

There are two sets of examples here, issuers and clients. Issuers are examples of setting up an OpenAuth server. The clients are examples of using OpenAuth in a client application and work with any of the issuer servers.

The fastest way to play around is to use the bun issuer. You can bring it up with:

```shell
$ bun run --hot ./issuer/bun/issuer.ts
```

You might have to install some workspace packages first, run this in the root:

```shell
$ bun install
$ cd packages/openauth
$ bun run build
```

This will bring it up on port 3000. Then try one of the clients - for example the astro one.

```
$ cd client/astro
$ bun dev
```

Now visit `http://localhost:4321` (the astro app) and experience the auth flow.

Or head over to `http://localhost:3000/password/authorize` to try the password flow directly.


================================================
FILE: examples/client/astro/.gitignore
================================================
# build output
dist/

# generated types
.astro/

# dependencies
node_modules/

# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# environment variables
.env
.env.production

# macOS-specific files
.DS_Store

# jetbrains setting folder
.idea/


================================================
FILE: examples/client/astro/.vscode/extensions.json
================================================
{
  "recommendations": ["astro-build.astro-vscode"],
  "unwantedRecommendations": []
}


================================================
FILE: examples/client/astro/.vscode/launch.json
================================================
{
  "version": "0.2.0",
  "configurations": [
    {
      "command": "./node_modules/.bin/astro dev",
      "name": "Development server",
      "request": "launch",
      "type": "node-terminal"
    }
  ]
}


================================================
FILE: examples/client/astro/README.md
================================================
# OpenAuth Astro Client

The files to note are

- `src/auth.ts` - creates the client that is used to interact with the auth server
- `src/middleware.ts` - middleware that runs to verify access tokens, refresh them if out of date, and redirect the user to the auth server if they are not logged in
- `src/pages/callback.ts` - the callback endpoint that receives the auth code and exchanges it for an access/refresh token


================================================
FILE: examples/client/astro/astro.config.mjs
================================================
// @ts-check
import { defineConfig } from "astro/config";

// https://astro.build/config
export default defineConfig({
  output: "server",
  server: {
    host: "0.0.0.0",
  },
});


================================================
FILE: examples/client/astro/package.json
================================================
{
  "name": "@openauthjs/example-client-astro",
  "type": "module",
  "version": "0.0.0",
  "scripts": {
    "dev": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "astro": "astro"
  },
  "dependencies": {
    "@openauthjs/openauth": "workspace:*",
    "astro": "5.0.2"
  }
}


================================================
FILE: examples/client/astro/src/auth.ts
================================================
import { createClient } from "@openauthjs/openauth/client"
import type { APIContext } from "astro"
export { subjects } from "../../../subjects"

export const client = createClient({
  clientID: "astro",
  issuer: "http://localhost:3000",
})

export function setTokens(ctx: APIContext, access: string, refresh: string) {
  ctx.cookies.set("refresh_token", refresh, {
    httpOnly: true,
    sameSite: "lax",
    path: "/",
    maxAge: 34560000,
  })
  ctx.cookies.set("access_token", access, {
    httpOnly: true,
    sameSite: "lax",
    path: "/",
    maxAge: 34560000,
  })
}


================================================
FILE: examples/client/astro/src/components/Welcome.astro
================================================
---
import astroLogo from '../assets/astro.svg';
import background from '../assets/background.svg';
---

<div id="container">
	<img id="background" src={background.src} alt="" fetchpriority="high" />
	<main>
		<section id="hero">
			<a href="https://astro.build"
				><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
			>
			<h1>
				To get started, open the <code><pre>src/pages</pre></code> directory in your project.
			</h1>
			<section id="links">
				<a class="button" href="https://docs.astro.build">Read our docs</a>
				<a href="https://astro.build/chat"
					>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
						><path
							fill="currentColor"
							d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
						></path></svg
					>
				</a>
			</section>
		</section>
	</main>

	<a href="https://astro.build/blog/astro-5/" id="news" class="box">
		<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
			><path
				d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
				fill="#111827"></path></svg
		>
		<h2>What's New in Astro 5.0?</h2>
		<p>
			From content layers to server islands, click to learn more about the new features and
			improvements in Astro 5.0
		</p>
	</a>
</div>

<style>
	#background {
		position: fixed;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		z-index: -1;
		filter: blur(100px);
	}

	#container {
		font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
		height: 100%;
	}

	main {
		height: 100%;
		display: flex;
		justify-content: center;
	}

	#hero {
		display: flex;
		align-items: start;
		flex-direction: column;
		justify-content: center;
		padding: 16px;
	}

	h1 {
		font-size: 22px;
		margin-top: 0.25em;
	}

	#links {
		display: flex;
		gap: 16px;
	}

	#links a {
		display: flex;
		align-items: center;
		padding: 10px 12px;
		color: #111827;
		text-decoration: none;
		transition: color 0.2s;
	}

	#links a:hover {
		color: rgb(78, 80, 86);
	}

	#links a svg {
		height: 1em;
		margin-left: 8px;
	}

	#links a.button {
		color: white;
		background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
		box-shadow:
			inset 0 0 0 1px rgba(255, 255, 255, 0.12),
			inset 0 -2px 0 rgba(0, 0, 0, 0.24);
		border-radius: 10px;
	}

	#links a.button:hover {
		color: rgb(230, 230, 230);
		box-shadow: none;
	}

	pre {
		font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas,
			'DejaVu Sans Mono', monospace;
		font-weight: normal;
		background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
		-webkit-background-clip: text;
		-webkit-text-fill-color: transparent;
		background-clip: text;
		margin: 0;
	}

	h2 {
		margin: 0 0 1em;
		font-weight: normal;
		color: #111827;
		font-size: 20px;
	}

	p {
		color: #4b5563;
		font-size: 16px;
		line-height: 24px;
		letter-spacing: -0.006em;
		margin: 0;
	}

	code {
		display: inline-block;
		background:
			linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
			linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
		border-radius: 8px;
		border: 1px solid transparent;
		padding: 6px 8px;
	}

	.box {
		padding: 16px;
		background: rgba(255, 255, 255, 1);
		border-radius: 16px;
		border: 1px solid white;
	}

	#news {
		position: absolute;
		bottom: 16px;
		right: 16px;
		max-width: 300px;
		text-decoration: none;
		transition: background 0.2s;
		backdrop-filter: blur(50px);
	}

	#news:hover {
		background: rgba(255, 255, 255, 0.55);
	}

	@media screen and (max-height: 368px) {
		#news {
			display: none;
		}
	}

	@media screen and (max-width: 768px) {
		#container {
			display: flex;
			flex-direction: column;
		}

		#hero {
			display: block;
			padding-top: 10%;
		}

		#links {
			flex-wrap: wrap;
		}

		#links a.button {
			padding: 14px 18px;
		}

		#news {
			right: 16px;
			left: 16px;
			bottom: 2.5rem;
			max-width: 100%;
		}

		h1 {
			line-height: 1.5;
		}
	}
</style>


================================================
FILE: examples/client/astro/src/env.d.ts
================================================
import type { SubjectPayload } from "@openauthjs/openauth/subject"
import { subjects } from "./auth"

declare global {
  declare namespace App {
    interface Locals {
      subject?: SubjectPayload<typeof subjects>
    }
  }
}


================================================
FILE: examples/client/astro/src/layouts/Layout.astro
================================================
<!doctype html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width" />
		<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
		<meta name="generator" content={Astro.generator} />
		<title>Astro Basics</title>
	</head>
	<body>
		<slot />
	</body>
</html>

<style>
	html,
	body {
		margin: 0;
		width: 100%;
		height: 100%;
	}
</style>


================================================
FILE: examples/client/astro/src/middleware.ts
================================================
import { defineMiddleware } from "astro:middleware"
import { subjects } from "../../../subjects"
import { client, setTokens } from "./auth"

export const onRequest = defineMiddleware(async (ctx, next) => {
  if (ctx.routePattern === "/callback") {
    return next()
  }

  try {
    const accessToken = ctx.cookies.get("access_token")
    if (accessToken) {
      const refreshToken = ctx.cookies.get("refresh_token")
      const verified = await client.verify(subjects, accessToken.value, {
        refresh: refreshToken?.value,
      })
      if (!verified.err) {
        if (verified.tokens)
          setTokens(ctx, verified.tokens.access, verified.tokens.refresh)
        ctx.locals.subject = verified.subject
        return next()
      }
    }
  } catch (e) {}

  const { url } = await client.authorize(
    new URL(ctx.request.url).origin + "/callback",
    "code",
  )
  return Response.redirect(url, 302)
})


================================================
FILE: examples/client/astro/src/pages/callback.ts
================================================
import type { APIRoute } from "astro"
import { client, setTokens } from "../auth"

export const GET: APIRoute = async (ctx) => {
  const code = ctx.url.searchParams.get("code")
  try {
    const tokens = await client.exchange(code!, ctx.url.origin + "/callback")
    if (!tokens.err) {
      setTokens(ctx, tokens.tokens.access, tokens.tokens.refresh)
    } else {
      throw tokens.err
    }
    return ctx.redirect("/", 302)
  } catch (e) {
    return Response.json(e, {
      status: 400,
    })
  }
}


================================================
FILE: examples/client/astro/src/pages/index.astro
================================================
---
import Welcome from '../components/Welcome.astro';
import Layout from '../layouts/Layout.astro';

// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
---

<Layout>
  Hello {Astro.locals.subject?.properties.id}
</Layout>


================================================
FILE: examples/client/astro/tsconfig.json
================================================
{
  "extends": "astro/tsconfigs/strict",
  "include": [".astro/types.d.ts", "**/*"],
  "exclude": ["dist"]
}


================================================
FILE: examples/client/cloudflare-api/api.ts
================================================
import type { Service } from "@cloudflare/workers-types"
import { createClient } from "@openauthjs/openauth/client"
import { subjects } from "../../subjects"

interface Env {
  OPENAUTH_ISSUER: string
  Auth: Service
  CloudflareAuth: Service
}

export default {
  async fetch(request: Request, env: Env) {
    const client = createClient({
      clientID: "cloudflare-api",
      // enables worker to worker communication if issuer is also a worker
      fetch: (input, init) => env.CloudflareAuth.fetch(input, init),
      issuer: env.OPENAUTH_ISSUER,
    })
    const url = new URL(request.url)
    const redirectURI = url.origin + "/callback"

    switch (url.pathname) {
      case "/callback":
        try {
          const code = url.searchParams.get("code")!
          const exchanged = await client.exchange(code, redirectURI)
          if (exchanged.err) throw new Error("Invalid code")
          const response = new Response(null, { status: 302, headers: {} })
          response.headers.set("Location", url.origin)
          setSession(
            response,
            exchanged.tokens.access,
            exchanged.tokens.refresh,
          )
          return response
        } catch (e: any) {
          return new Response(e.toString())
        }
      case "/authorize":
        return Response.redirect(
          await client.authorize(redirectURI, "code").then((v) => v.url),
          302,
        )
      case "/":
        const cookies = new URLSearchParams(
          request.headers.get("cookie")?.replaceAll("; ", "&"),
        )
        const verified = await client.verify(
          subjects,
          cookies.get("access_token")!,
          {
            refresh: cookies.get("refresh_token") || undefined,
          },
        )
        if (verified.err)
          return Response.redirect(url.origin + "/authorize", 302)
        const resp = Response.json(verified.subject)
        if (verified.tokens)
          setSession(resp, verified.tokens.access, verified.tokens.refresh)
        return resp
      default:
        return new Response("Not found", { status: 404 })
    }
  },
}

function setSession(response: Response, access: string, refresh: string) {
  if (access) {
    response.headers.append(
      "Set-Cookie",
      `access_token=${access}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2147483647`,
    )
  }
  if (refresh) {
    response.headers.append(
      "Set-Cookie",
      `refresh_token=${refresh}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2147483647`,
    )
  }
}


================================================
FILE: examples/client/cloudflare-api/package.json
================================================
{
  "name": "cloudflare-api",
  "version": "0.0.0",
  "private": true
}


================================================
FILE: examples/client/jwt-api/CHANGELOG.md
================================================
# jwt-api

## 1.0.1

### Patch Changes

- Updated dependencies [8b5f490]
  - @openauthjs/openauth@0.2.4


================================================
FILE: examples/client/jwt-api/README.md
================================================
# JWT API

This simple API verifies the `Authorization` header using the OpenAuth client and returns the subject.

Run it using.

```bash
bun run --hot index.ts
```

Then visit `http://localhost:3001/` in your browser.

This works with the [React Client](../react) example that makes a call to this API after the auth flow.


================================================
FILE: examples/client/jwt-api/index.ts
================================================
import { createClient } from "@openauthjs/openauth/client"
import { subjects } from "../../subjects"

const headers = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "*",
  "Access-Control-Allow-Methods": "*",
}

const client = createClient({
  clientID: "jwt-api",
  issuer: "http://localhost:3000",
})

const server = Bun.serve({
  port: 3001,
  async fetch(req) {
    const url = new URL(req.url)

    if (req.method === "OPTIONS") {
      return new Response(null, { headers })
    }

    if (url.pathname === "/" && req.method === "GET") {
      const authHeader = req.headers.get("Authorization")

      if (!authHeader) {
        return new Response("401", { headers, status: 401 })
      }

      const token = authHeader.split(" ")[1]
      const verified = await client.verify(subjects, token)

      if (verified.err) {
        return new Response("401", { headers, status: 401 })
      }

      return new Response(verified.subject.properties.id, { headers })
    }

    return new Response("404", { status: 404 })
  },
})

console.log(`Listening on ${server.url}`)


================================================
FILE: examples/client/jwt-api/package.json
================================================
{
  "name": "@openauthjs/example-jwt-api",
  "version": "1.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@openauthjs/openauth": "workspace:*"
  }
}


================================================
FILE: examples/client/lambda-api/api.ts
================================================
import { Context, Hono } from "hono"
import { getCookie, setCookie } from "hono/cookie"
import { createClient } from "@openauthjs/openauth/client"
import { handle } from "hono/aws-lambda"
import { subjects } from "../../subjects"

const client = createClient({
  clientID: "lambda-api",
})

const app = new Hono()
  .get("/authorize", async (c) => {
    const origin = new URL(c.req.url).origin
    const { url } = await client.authorize(origin + "/callback", "code")
    return c.redirect(url, 302)
  })
  .get("/callback", async (c) => {
    const origin = new URL(c.req.url).origin
    try {
      const code = c.req.query("code")
      if (!code) throw new Error("Missing code")
      const exchanged = await client.exchange(code, origin + "/callback")
      if (exchanged.err)
        return new Response(exchanged.err.toString(), {
          status: 400,
        })
      setSession(c, exchanged.tokens.access, exchanged.tokens.refresh)
      return c.redirect("/", 302)
    } catch (e: any) {
      return new Response(e.toString())
    }
  })
  .get("/", async (c) => {
    const access = getCookie(c, "access_token")
    const refresh = getCookie(c, "refresh_token")
    try {
      const verified = await client.verify(subjects, access!, {
        refresh,
      })
      if (verified.err) throw new Error("Invalid access token")
      if (verified.tokens)
        setSession(c, verified.tokens.access, verified.tokens.refresh)
      return c.json(verified.subject)
    } catch (e) {
      console.error(e)
      return c.redirect("/authorize", 302)
    }
  })

export const handler = handle(app)

function setSession(c: Context, accessToken?: string, refreshToken?: string) {
  if (accessToken) {
    setCookie(c, "access_token", accessToken, {
      httpOnly: true,
      sameSite: "Strict",
      path: "/",
      maxAge: 34560000,
    })
  }
  if (refreshToken) {
    setCookie(c, "refresh_token", refreshToken, {
      httpOnly: true,
      sameSite: "Strict",
      path: "/",
      maxAge: 34560000,
    })
  }
}


================================================
FILE: examples/client/lambda-api/package.json
================================================
{
  "name": "lambda-api",
  "version": "0.0.0",
  "private": true
}


================================================
FILE: examples/client/nextjs/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts


================================================
FILE: examples/client/nextjs/CHANGELOG.md
================================================
# nextjs

## 0.1.6

### Patch Changes

- Updated dependencies [8b5f490]
  - @openauthjs/openauth@0.2.4

## 0.1.5

### Patch Changes

- Updated dependencies [80238de]
  - @openauthjs/openauth@0.2.3

## 0.1.4

### Patch Changes

- Updated dependencies [6da8647]
  - @openauthjs/openauth@0.2.2

## 0.1.3

### Patch Changes

- Updated dependencies [83125f1]
  - @openauthjs/openauth@0.2.1

## 0.1.2

### Patch Changes

- Updated dependencies [8c3f050]
- Updated dependencies [0f93def]
  - @openauthjs/openauth@0.2.0

## 0.1.1

### Patch Changes

- Updated dependencies [584728f]
- Updated dependencies [41acdc2]
- Updated dependencies [2aa531b]
  - @openauthjs/openauth@0.1.2


================================================
FILE: examples/client/nextjs/README.md
================================================
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

Make sure your OpenAuth server is running at `http://localhost:3000`.

Then start the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3001](http://localhost:3001) with your browser and click **Login with OpenAuth** to start the auth flow.

## Files

- [`app/auth.ts`](app/auth.ts): OpenAuth client and helper to set tokens in cookies.
- [`app/actions.ts`](app/actions.ts): Actions to get current logged in user, and to login and logout.
- [`app/api/callback/route.ts`](app/api/callback/route.ts): Callback route for OpenAuth.
- [`app/page.tsx`](app/page.tsx): Shows login and logout buttons and the current user.


================================================
FILE: examples/client/nextjs/app/actions.ts
================================================
"use server"

import { redirect } from "next/navigation"
import { headers as getHeaders, cookies as getCookies } from "next/headers"
import { client, subjects, setTokens } from "./auth"

export async function auth() {
  const cookies = await getCookies()
  const accessToken = cookies.get("access_token")
  const refreshToken = cookies.get("refresh_token")

  if (!accessToken) {
    return false
  }

  const verified = await client.verify(subjects, accessToken.value, {
    refresh: refreshToken?.value,
  })

  if (verified.err) {
    return false
  }
  if (verified.tokens) {
    await setTokens(verified.tokens.access, verified.tokens.refresh)
  }

  return verified.subject
}

export async function login() {
  const cookies = await getCookies()
  const accessToken = cookies.get("access_token")
  const refreshToken = cookies.get("refresh_token")

  if (accessToken) {
    const verified = await client.verify(subjects, accessToken.value, {
      refresh: refreshToken?.value,
    })
    if (!verified.err && verified.tokens) {
      await setTokens(verified.tokens.access, verified.tokens.refresh)
      redirect("/")
    }
  }

  const headers = await getHeaders()
  const host = headers.get("host")
  const protocol = host?.includes("localhost") ? "http" : "https"
  const { url } = await client.authorize(
    `${protocol}://${host}/api/callback`,
    "code",
  )
  redirect(url)
}

export async function logout() {
  const cookies = await getCookies()
  cookies.delete("access_token")
  cookies.delete("refresh_token")

  redirect("/")
}


================================================
FILE: examples/client/nextjs/app/api/callback/route.ts
================================================
import { client, setTokens } from "../../auth"
import { type NextRequest, NextResponse } from "next/server"

export async function GET(req: NextRequest) {
  const url = new URL(req.url)
  const code = url.searchParams.get("code")
  const exchanged = await client.exchange(code!, `${url.origin}/api/callback`)
  if (exchanged.err) return NextResponse.json(exchanged.err, { status: 400 })
  await setTokens(exchanged.tokens.access, exchanged.tokens.refresh)
  return NextResponse.redirect(`${url.origin}/`)
}


================================================
FILE: examples/client/nextjs/app/auth.ts
================================================
import { createClient } from "@openauthjs/openauth/client"
import { cookies as getCookies } from "next/headers"
export { subjects } from "../../../subjects"

export const client = createClient({
  clientID: "nextjs",
  issuer: "http://localhost:3000",
})

export async function setTokens(access: string, refresh: string) {
  const cookies = await getCookies()

  cookies.set({
    name: "access_token",
    value: access,
    httpOnly: true,
    sameSite: "lax",
    path: "/",
    maxAge: 34560000,
  })
  cookies.set({
    name: "refresh_token",
    value: refresh,
    httpOnly: true,
    sameSite: "lax",
    path: "/",
    maxAge: 34560000,
  })
}


================================================
FILE: examples/client/nextjs/app/globals.css
================================================
:root {
  --background: #ffffff;
  --foreground: #171717;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}

html,
body {
  max-width: 100vw;
  overflow-x: hidden;
}

body {
  color: var(--foreground);
  background: var(--background);
  font-family: Arial, Helvetica, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

@media (prefers-color-scheme: dark) {
  html {
    color-scheme: dark;
  }
}


================================================
FILE: examples/client/nextjs/app/layout.tsx
================================================
import type { Metadata } from "next"
import { Geist, Geist_Mono } from "next/font/google"
import "./globals.css"

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
})

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
})

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable}`}>
        {children}
      </body>
    </html>
  )
}


================================================
FILE: examples/client/nextjs/app/page.module.css
================================================
.page {
  --gray-rgb: 0, 0, 0;
  --gray-alpha-200: rgba(var(--gray-rgb), 0.08);
  --gray-alpha-100: rgba(var(--gray-rgb), 0.05);

  --button-primary-hover: #383838;
  --button-secondary-hover: #f2f2f2;

  display: grid;
  grid-template-rows: 20px 1fr 20px;
  align-items: center;
  justify-items: center;
  min-height: 100svh;
  padding: 80px;
  gap: 64px;
  font-family: var(--font-geist-sans);
}

@media (prefers-color-scheme: dark) {
  .page {
    --gray-rgb: 255, 255, 255;
    --gray-alpha-200: rgba(var(--gray-rgb), 0.145);
    --gray-alpha-100: rgba(var(--gray-rgb), 0.06);

    --button-primary-hover: #ccc;
    --button-secondary-hover: #1a1a1a;
  }
}

.main {
  display: flex;
  flex-direction: column;
  gap: 32px;
  grid-row-start: 2;
}

.main ol {
  font-family: var(--font-geist-mono);
  padding-left: 0;
  margin: 0;
  font-size: 14px;
  line-height: 24px;
  letter-spacing: -0.01em;
  list-style-position: inside;
}

.main li:not(:last-of-type) {
  margin-bottom: 8px;
}

.main code {
  font-family: inherit;
  background: var(--gray-alpha-100);
  padding: 2px 4px;
  border-radius: 4px;
  font-weight: 600;
}

.ctas {
  display: flex;
  gap: 16px;
}

.ctas button {
  appearance: none;
  background: transparent;
  border-radius: 128px;
  height: 48px;
  padding: 0 20px;
  border: none;
  border: 1px solid transparent;
  transition:
    background 0.2s,
    color 0.2s,
    border-color 0.2s;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  line-height: 20px;
  font-weight: 500;
}

button.primary {
  background: var(--foreground);
  color: var(--background);
  gap: 8px;
}

button.secondary {
  border-color: var(--gray-alpha-200);
  min-width: 180px;
}

.footer {
  grid-row-start: 3;
  display: flex;
  gap: 24px;
}

.footer a {
  display: flex;
  align-items: center;
  gap: 8px;
}

.footer img {
  flex-shrink: 0;
}

/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
  a.primary:hover {
    background: var(--button-primary-hover);
    border-color: transparent;
  }

  a.secondary:hover {
    background: var(--button-secondary-hover);
    border-color: transparent;
  }

  .footer a:hover {
    text-decoration: underline;
    text-underline-offset: 4px;
  }
}

@media (max-width: 600px) {
  .page {
    padding: 32px;
    padding-bottom: 80px;
  }

  .main {
    align-items: center;
  }

  .main ol {
    text-align: center;
  }

  .ctas {
    flex-direction: column;
  }

  .ctas a {
    font-size: 14px;
    height: 40px;
    padding: 0 16px;
  }

  a.secondary {
    min-width: auto;
  }

  .footer {
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
  }
}

@media (prefers-color-scheme: dark) {
  .logo {
    filter: invert();
  }
}


================================================
FILE: examples/client/nextjs/app/page.tsx
================================================
import Image from "next/image"
import { auth, login, logout } from "./actions"
import styles from "./page.module.css"

export default async function Home() {
  const subject = await auth()

  return (
    <div className={styles.page}>
      <main className={styles.main}>
        <Image
          className={styles.logo}
          src="/next.svg"
          alt="Next.js logo"
          width={180}
          height={38}
          priority
        />
        <ol>
          {subject ? (
            <>
              <li>
                Logged in as <code>{subject.properties.id}</code>.
              </li>
              <li>
                And then check out <code>app/page.tsx</code>.
              </li>
            </>
          ) : (
            <>
              <li>Login with your email and password.</li>
              <li>
                And then check out <code>app/page.tsx</code>.
              </li>
            </>
          )}
        </ol>

        <div className={styles.ctas}>
          {subject ? (
            <form action={logout}>
              <button className={styles.secondary}>Logout</button>
            </form>
          ) : (
            <form action={login}>
              <button className={styles.primary}>Login with OpenAuth</button>
            </form>
          )}
        </div>
      </main>
      <footer className={styles.footer}>
        <a
          href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          <Image
            aria-hidden
            src="/file.svg"
            alt="File icon"
            width={16}
            height={16}
          />
          Learn
        </a>
        <a
          href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          <Image
            aria-hidden
            src="/window.svg"
            alt="Window icon"
            width={16}
            height={16}
          />
          Examples
        </a>
        <a
          href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          <Image
            aria-hidden
            src="/globe.svg"
            alt="Globe icon"
            width={16}
            height={16}
          />
          Go to nextjs.org →
        </a>
      </footer>
    </div>
  )
}


================================================
FILE: examples/client/nextjs/next.config.ts
================================================
import type { NextConfig } from "next"

const nextConfig: NextConfig = {
  /* config options here */
}

export default nextConfig


================================================
FILE: examples/client/nextjs/package.json
================================================
{
  "name": "@openauthjs/example-nextjs",
  "version": "0.1.6",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@openauthjs/openauth": "workspace:*",
    "next": "15.1.0",
    "react": "19.0.0",
    "react-dom": "19.0.0"
  },
  "devDependencies": {
    "@types/node": "22.10.1",
    "@types/react": "19.0.1",
    "@types/react-dom": "19.0.2",
    "typescript": "5.6.3"
  }
}


================================================
FILE: examples/client/nextjs/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}


================================================
FILE: examples/client/react/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: examples/client/react/README.md
================================================
# React SPA Auth

This uses the token + pkce flow to authenticate a user. Start it using.

```bash
bun run dev
```

Then visit `http://localhost:5173` in your browser.

It needs the OpenAuth server running at `http://localhost:3000`. Start it from the `examples/` dir using.

```bash
bun run --hot issuer/bun/issuer.ts
```

You might have to install some workspace packages first, run this in the root:

```bash
$ bun install
$ cd packages/openauth
$ bun run build
```

And optionally a JWT API running to get the user subject on `http://localhost:3001`. Start it using.

```bash
bun run --hot client/jwt-api/index.ts
```


================================================
FILE: examples/client/react/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>


================================================
FILE: examples/client/react/package.json
================================================
{
  "name": "@openauthjs/example-react",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@openauthjs/openauth": "workspace:*",
    "react": "19.0.0",
    "react-dom": "19.0.0"
  },
  "devDependencies": {
    "@eslint/js": "^9.15.0",
    "@types/react": "19.0.1",
    "@types/react-dom": "19.0.2",
    "@vitejs/plugin-react": "^4.3.4",
    "eslint": "^9.15.0",
    "eslint-plugin-react-hooks": "^5.0.0",
    "eslint-plugin-react-refresh": "^0.4.14",
    "globals": "^15.12.0",
    "typescript": "5.6.3",
    "typescript-eslint": "^8.15.0",
    "vite": "^6.0.1"
  }
}


================================================
FILE: examples/client/react/src/App.tsx
================================================
import { useState } from "react"
import { useAuth } from "./AuthContext"

function App() {
  const auth = useAuth()
  const [status, setStatus] = useState("")

  async function callApi() {
    const res = await fetch("http://localhost:3001/", {
      headers: {
        Authorization: `Bearer ${await auth.getToken()}`,
      },
    })

    setStatus(res.ok ? "success" : "error")
  }

  return !auth.loaded ? (
    <div>Loading...</div>
  ) : (
    <div>
      {auth.loggedIn ? (
        <div>
          <p>
            <span>Logged in</span>
            {auth.userId && <span> as {auth.userId}</span>}
          </p>
          {status !== "" && <p>API call: {status}</p>}
          <button onClick={callApi}>Call API</button>
          <button onClick={auth.logout}>Logout</button>
        </div>
      ) : (
        <button onClick={auth.login}>Login with OAuth</button>
      )}
    </div>
  )
}

export default App


================================================
FILE: examples/client/react/src/AuthContext.tsx
================================================
import {
  useRef,
  useState,
  ReactNode,
  useEffect,
  useContext,
  createContext,
} from "react"
import { createClient } from "@openauthjs/openauth/client"

const client = createClient({
  clientID: "react",
  issuer: "http://localhost:3000",
})

interface AuthContextType {
  userId?: string
  loaded: boolean
  loggedIn: boolean
  logout: () => void
  login: () => Promise<void>
  getToken: () => Promise<string | undefined>
}

const AuthContext = createContext({} as AuthContextType)

export function AuthProvider({ children }: { children: ReactNode }) {
  const initializing = useRef(true)
  const [loaded, setLoaded] = useState(false)
  const [loggedIn, setLoggedIn] = useState(false)
  const token = useRef<string | undefined>(undefined)
  const [userId, setUserId] = useState<string | undefined>()

  useEffect(() => {
    const hash = new URLSearchParams(location.search.slice(1))
    const code = hash.get("code")
    const state = hash.get("state")

    if (!initializing.current) {
      return
    }

    initializing.current = false

    if (code && state) {
      callback(code, state)
      return
    }

    auth()
  }, [])

  async function auth() {
    const token = await refreshTokens()

    if (token) {
      await user()
    }

    setLoaded(true)
  }

  async function refreshTokens() {
    const refresh = localStorage.getItem("refresh")
    if (!refresh) return
    const next = await client.refresh(refresh, {
      access: token.current,
    })
    if (next.err) return
    if (!next.tokens) return token.current

    localStorage.setItem("refresh", next.tokens.refresh)
    token.current = next.tokens.access

    return next.tokens.access
  }

  async function getToken() {
    const token = await refreshTokens()

    if (!token) {
      await login()
      return
    }

    return token
  }

  async function login() {
    const { challenge, url } = await client.authorize(location.origin, "code", {
      pkce: true,
    })
    sessionStorage.setItem("challenge", JSON.stringify(challenge))
    location.href = url
  }

  async function callback(code: string, state: string) {
    const challenge = JSON.parse(sessionStorage.getItem("challenge")!)
    if (code) {
      if (state === challenge.state && challenge.verifier) {
        const exchanged = await client.exchange(
          code!,
          location.origin,
          challenge.verifier,
        )
        if (!exchanged.err) {
          token.current = exchanged.tokens?.access
          localStorage.setItem("refresh", exchanged.tokens.refresh)
        }
      }
      window.location.replace("/")
    }
  }

  async function user() {
    const res = await fetch("http://localhost:3001/", {
      headers: {
        Authorization: `Bearer ${token.current}`,
      },
    })

    if (res.ok) {
      setUserId(await res.text())
      setLoggedIn(true)
    }
  }

  function logout() {
    localStorage.removeItem("refresh")
    token.current = undefined

    window.location.replace("/")
  }

  return (
    <AuthContext.Provider
      value={{
        login,
        logout,
        userId,
        loaded,
        loggedIn,
        getToken,
      }}
    >
      {children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  return useContext(AuthContext)
}


================================================
FILE: examples/client/react/src/main.tsx
================================================
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { AuthProvider } from "./AuthContext"
import App from "./App"

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <AuthProvider>
      <App />
    </AuthProvider>
  </StrictMode>,
)


================================================
FILE: examples/client/react/src/vite-env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: examples/client/react/tsconfig.app.json
================================================
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["src"]
}


================================================
FILE: examples/client/react/tsconfig.json
================================================
{
  "files": [],
  "references": [
    {
      "path": "./tsconfig.app.json"
    },
    {
      "path": "./tsconfig.node.json"
    }
  ]
}


================================================
FILE: examples/client/react/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
    "target": "ES2022",
    "lib": ["ES2023"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: examples/client/react/vite.config.ts
================================================
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
})


================================================
FILE: examples/client/sveltekit/.npmrc
================================================
engine-strict=true


================================================
FILE: examples/client/sveltekit/package.json
================================================
{
  "name": "@openauthjs/example-client-sveltekit",
  "type": "module",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "preview": "vite preview",
    "prepare": "svelte-kit sync || echo ''",
    "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
    "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
  },
  "devDependencies": {
    "@openauthjs/openauth": "^0.4.3",
    "@sveltejs/adapter-auto": "^4.0.0",
    "@sveltejs/kit": "^2.16.0",
    "@sveltejs/vite-plugin-svelte": "^5.0.0",
    "svelte": "^5.0.0",
    "svelte-check": "^4.0.0",
    "typescript": "5.6.3",
    "valibot": "1.0.0-beta.15",
    "vite": "^6.0.1"
  }
}


================================================
FILE: examples/client/sveltekit/src/app.d.ts
================================================
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
  namespace App {
    // interface Error {}
    interface Locals {
      session: { id: string }
    }
    // interface PageData {}
    // interface PageState {}
    // interface Platform {}
  }
}

export {}


================================================
FILE: examples/client/sveltekit/src/app.html
================================================
<!doctype html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<link rel="icon" href="%sveltekit.assets%/favicon.png" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		%sveltekit.head%
	</head>
	<body data-sveltekit-preload-data="hover">
		<div style="display: contents">%sveltekit.body%</div>
	</body>
</html>


================================================
FILE: examples/client/sveltekit/src/hooks.server.ts
================================================
import { redirect, type Handle } from "@sveltejs/kit"
import { createAuthClient, setTokens } from "$lib/auth.server"
import { subjects } from "../../../subjects"

export const handle: Handle = async ({ event, resolve }) => {
  if (event.url.pathname === "/callback") {
    return resolve(event)
  }

  const client = createAuthClient(event)
  try {
    const accessToken = event.cookies.get("access_token")
    if (accessToken) {
      const refreshToken = event.cookies.get("refresh_token")
      const verified = await client.verify(subjects, accessToken, {
        refresh: refreshToken,
      })
      if (!verified.err) {
        if (verified.tokens)
          setTokens(event, verified.tokens.access, verified.tokens.refresh)
        event.locals.session = verified.subject.properties
        return resolve(event)
      }
    }
  } catch (e) {}

  const { url } = await client.authorize(event.url.origin + "/callback", "code")
  return redirect(302, url)
}


================================================
FILE: examples/client/sveltekit/src/lib/auth.server.ts
================================================
import { createClient } from "@openauthjs/openauth/client"
import type { RequestEvent } from "@sveltejs/kit"

export function createAuthClient(event: RequestEvent) {
  return createClient({
    clientID: "openauth-sveltekit-example",
    issuer: "http://localhost:3000",
    fetch: event.fetch,
  })
}

export function setTokens(
  event: RequestEvent,
  access: string,
  refresh: string,
) {
  event.cookies.set("refresh_token", refresh, {
    httpOnly: true,
    sameSite: "lax",
    path: "/",
    maxAge: 34560000,
  })
  event.cookies.set("access_token", access, {
    httpOnly: true,
    sameSite: "lax",
    path: "/",
    maxAge: 34560000,
  })
}


================================================
FILE: examples/client/sveltekit/src/routes/+page.server.ts
================================================
export async function load(event) {
  return {
    subject: event.locals.session,
  }
}


================================================
FILE: examples/client/sveltekit/src/routes/+page.svelte
================================================
<script lang="ts">
    const { data } = $props();
</script>

<h1>Hello {data.subject.id}</h1>


================================================
FILE: examples/client/sveltekit/src/routes/callback/+server.ts
================================================
import { redirect } from "@sveltejs/kit"
import { createAuthClient, setTokens } from "$lib/auth.server.js"

export async function GET(event) {
  const code = event.url.searchParams.get("code")
  const authClient = createAuthClient(event)
  const tokens = await authClient.exchange(
    code!,
    event.url.origin + "/callback",
  )
  if (!tokens.err) {
    setTokens(event, tokens.tokens.access, tokens.tokens.refresh)
  } else {
    throw tokens.err
  }
  return redirect(302, `/`)
}


================================================
FILE: examples/client/sveltekit/svelte.config.js
================================================
import adapter from "@sveltejs/adapter-auto"
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://svelte.dev/docs/kit/integrations
  // for more information about preprocessors
  preprocess: vitePreprocess(),

  kit: {
    // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
    // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
    // See https://svelte.dev/docs/kit/adapters for more information about adapters.
    adapter: adapter(),
  },
}

export default config


================================================
FILE: examples/client/sveltekit/tsconfig.json
================================================
{
  "extends": "./.svelte-kit/tsconfig.json",
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "moduleResolution": "bundler"
  }
  // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
  // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
  //
  // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
  // from the referenced tsconfig.json - TypeScript does not merge them in
}


================================================
FILE: examples/client/sveltekit/vite.config.ts
================================================
import { sveltekit } from "@sveltejs/kit/vite"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [sveltekit()],
})


================================================
FILE: examples/issuer/bun/.gitignore
================================================
persist.json


================================================
FILE: examples/issuer/bun/issuer.ts
================================================
import { issuer } from "@openauthjs/openauth"
import { MemoryStorage } from "@openauthjs/openauth/storage/memory"
import { PasswordProvider } from "@openauthjs/openauth/provider/password"
import { PasswordUI } from "@openauthjs/openauth/ui/password"
import { subjects } from "../../subjects.js"

async function getUser(email: string) {
  // Get user from database
  // Return user ID
  return "123"
}

export default issuer({
  subjects,
  storage: MemoryStorage({
    persist: "./persist.json",
  }),
  providers: {
    password: PasswordProvider(
      PasswordUI({
        sendCode: async (email, code) => {
          console.log(email, code)
        },
        validatePassword: (password) => {
          if (password.length < 8) {
            return "Password must be at least 8 characters"
          }
        },
      }),
    ),
  },
  async allow() {
    return true
  },
  success: async (ctx, value) => {
    if (value.provider === "password") {
      return ctx.subject("user", {
        id: await getUser(value.email),
      })
    }
    throw new Error("Invalid provider")
  },
})


================================================
FILE: examples/issuer/bun/package.json
================================================
{
  "name": "@openauthjs/example-issuer-bun",
  "version": "0.0.0",
  "dependencies": {
    "@openauthjs/openauth": "workspace:*"
  }
}


================================================
FILE: examples/issuer/cloudflare/issuer.ts
================================================
import { issuer } from "@openauthjs/openauth"
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
import {
  type ExecutionContext,
  type KVNamespace,
} from "@cloudflare/workers-types"
import { subjects } from "../../subjects.js"
import { PasswordProvider } from "@openauthjs/openauth/provider/password"
import { PasswordUI } from "@openauthjs/openauth/ui/password"

interface Env {
  CloudflareAuthKV: KVNamespace
}

async function getUser(email: string) {
  // Get user from database
  // Return user ID
  return "123"
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    return issuer({
      storage: CloudflareStorage({
        namespace: env.CloudflareAuthKV,
      }),
      subjects,
      providers: {
        password: PasswordProvider(
          PasswordUI({
            sendCode: async (email, code) => {
              console.log(email, code)
            },
          }),
        ),
      },
      success: async (ctx, value) => {
        if (value.provider === "password") {
          return ctx.subject("user", {
            id: await getUser(value.email),
          })
        }
        throw new Error("Invalid provider")
      },
    }).fetch(request, env, ctx)
  },
}


================================================
FILE: examples/issuer/cloudflare/package.json
================================================
{
  "name": "@openauthjs/example-issuer-cloudflare",
  "version": "0.0.0",
  "dependencies": {
    "@openauthjs/openauth": "workspace:*",
    "sst": "3.5.1"
  }
}


================================================
FILE: examples/issuer/cloudflare/sst-env.d.ts
================================================
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
import "sst"
export {}
declare module "sst" {
  export interface Resource {
    CloudflareAuth: {
      type: "sst.cloudflare.Worker"
      url: string
    }
    CloudflareAuthKV: {
      type: "sst.cloudflare.Kv"
    }
  }
}


================================================
FILE: examples/issuer/cloudflare/sst.config.ts
================================================
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
  app(input) {
    return {
      name: "openauth-example-cloudflare",
      removal: input?.stage === "production" ? "retain" : "remove",
      home: "cloudflare",
    }
  },
  async run() {
    // cloudflare
    const kv = new sst.cloudflare.Kv("CloudflareAuthKV")
    const auth = new sst.cloudflare.Worker("CloudflareAuth", {
      handler: "./issuer.ts",
      link: [kv],
      url: true,
    })

    return {
      url: auth.url,
    }
  },
})


================================================
FILE: examples/issuer/custom-frontend/auth/issuer.ts
================================================
import { issuer } from "@openauthjs/openauth"
import { MemoryStorage } from "@openauthjs/openauth/storage/memory"
import { CodeProvider } from "@openauthjs/openauth/provider/code"
import { subjects } from "../../../subjects.js"

async function getUser(email: string) {
  // Get user from database
  // Return user ID
  return "123"
}

export default issuer({
  subjects,
  storage: MemoryStorage({
    persist: "./persist.json",
  }),
  providers: {
    code: CodeProvider({
      sendCode: async (claims, code) => {
        console.log(claims.email, code)
      },
      async request(req, state, _form, error) {
        const url = new URL(`http://localhost:3001`)
        url.pathname = `/auth/${state.type}`
        if (error) url.searchParams.set("error", error.type)
        return new Response(null, {
          status: 302,
          headers: {
            Location: url.toString(),
          },
        })
      },
    }),
  },
  success: async (ctx, value) => {
    if (value.provider === "code") {
      return ctx.subject("user", {
        id: await getUser(value.claims.email),
      })
    }
    throw new Error("Invalid provider")
  },
})


================================================
FILE: examples/issuer/custom-frontend/auth/package.json
================================================
{
  "name": "@openauthjs/example-custom-frontend-issuer",
  "version": "0.0.0",
  "dependencies": {
    "@openauthjs/openauth": "workspace:*"
  }
}


================================================
FILE: examples/issuer/custom-frontend/frontend/frontend.tsx
================================================
/** @jsx jsx */
/** @jsxImportSource hono/jsx */

import { Hono } from "hono"
import { PropsWithChildren } from "hono/jsx"

function Layout(props: PropsWithChildren) {
  return (
    <html lang="en">
      <head>
        <meta charSet="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Issuer</title>
      </head>
      <body>{props.children}</body>
    </html>
  )
}

const app = new Hono()
  .get("/auth/start", async (c) => {
    return c.html(
      <Layout>
        <h1>Issuer</h1>
        <form method="post" action="http://localhost:3000/code/authorize">
          <input type="hidden" name="action" value="request" />
          <label for="email">Email</label>
          <input type="email" name="email" id="email" />
          <button type="submit">Request</button>
        </form>
      </Layout>,
    )
  })
  .get("/auth/code", async (c) => {
    return c.html(
      <Layout>
        <h1>Issuer</h1>
        <form method="post" action="http://localhost:3000/code/authorize">
          <input type="hidden" name="action" value="verify" />
          <label for="code">Code</label>
          <input
            type="text"
            name="code"
            id="code"
            minLength={6}
            maxLength={6}
          />
          <button type="submit">Verify</button>
        </form>
      </Layout>,
    )
  })

export default {
  port: 3001,
  fetch: app.fetch,
}


================================================
FILE: examples/issuer/custom-frontend/frontend/package.json
================================================
{
  "name": "@openauthjs/example-custom-frontend",
  "scripts": {
    "dev": "bun run --hot frontend.tsx"
  }
}


================================================
FILE: examples/issuer/custom-frontend/package.json
================================================
{
  "name": "custom-frontend",
  "version": "0.0.0",
  "private": true
}


================================================
FILE: examples/issuer/lambda/issuer.ts
================================================
import { issuer } from "@openauthjs/openauth"
import { handle } from "hono/aws-lambda"
import { subjects } from "../../subjects.js"
import { PasswordUI } from "@openauthjs/openauth/ui/password"
import { PasswordProvider } from "@openauthjs/openauth/provider/password"

async function getUser(email: string) {
  // Get user from database
  // Return user ID
  return "123"
}

const app = issuer({
  subjects,
  providers: {
    password: PasswordProvider(
      PasswordUI({
        sendCode: async (email, code) => {
          console.log(email, code)
        },
      }),
    ),
  },
  success: async (ctx, value) => {
    if (value.provider === "password") {
      return ctx.subject("user", {
        id: await getUser(value.email),
      })
    }
    throw new Error("Invalid provider")
  },
})

// @ts-ignore
export const handler = handle(app)


================================================
FILE: examples/issuer/lambda/package.json
================================================
{
  "name": "@openauthjs/example-issuer-aws",
  "version": "0.0.0",
  "dependencies": {
    "@openauthjs/openauth": "workspace:*",
    "sst": "3.5.1"
  }
}


================================================
FILE: examples/issuer/lambda/sst-env.d.ts
================================================
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
import "sst"
export {}
declare module "sst" {
  export interface Resource {}
}


================================================
FILE: examples/issuer/lambda/sst.config.ts
================================================
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
  app(input) {
    return {
      name: "openauth-example-lambda",
      removal: input?.stage === "production" ? "retain" : "remove",
      home: "aws",
    }
  },
  async run() {
    const auth = new sst.aws.Auth("Auth", {
      issuer: "./issuer.handler",
    })
  },
})


================================================
FILE: examples/issuer/node/.gitignore
================================================
persist.json


================================================
FILE: examples/issuer/node/authorizer.ts
================================================
import { issuer } from "@openauthjs/openauth"
import { MemoryStorage } from "@openauthjs/openauth/storage/memory"
import { PasswordUI } from "@openauthjs/openauth/ui/password"
import { serve } from "@hono/node-server"
import { subjects } from "../../subjects"
import { PasswordProvider } from "@openauthjs/openauth/provider/password"

async function getUser(email: string) {
  // Get user from database
  // Return user ID
  return "123"
}

const app = issuer({
  subjects,
  storage: MemoryStorage({
    persist: "./persist.json",
  }),
  providers: {
    password: PasswordProvider(
      PasswordUI({
        sendCode: async (email, code) => {
          console.log(email, code)
        },
      }),
    ),
  },
  success: async (ctx, value) => {
    if (value.provider === "password") {
      return ctx.subject("user", {
        id: await getUser(value.email),
      })
    }
    throw new Error("Invalid provider")
  },
})

serve(app)


================================================
FILE: examples/issuer/node/package.json
================================================
{
  "name": "node",
  "version": "0.0.0",
  "private": true
}


================================================
FILE: examples/quickstart/sst/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# sst
.sst

# open-next
.open-next


================================================
FILE: examples/quickstart/sst/README.md
================================================
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.


================================================
FILE: examples/quickstart/sst/app/actions.ts
================================================
"use server"

import { redirect } from "next/navigation"
import { headers as getHeaders, cookies as getCookies } from "next/headers"
import { subjects } from "../auth/subjects"
import { client, setTokens } from "./auth"

export async function auth() {
  const cookies = await getCookies()
  const accessToken = cookies.get("access_token")
  const refreshToken = cookies.get("refresh_token")

  if (!accessToken) {
    return false
  }

  const verified = await client.verify(subjects, accessToken.value, {
    refresh: refreshToken?.value,
  })

  if (verified.err) {
    return false
  }
  if (verified.tokens) {
    await setTokens(verified.tokens.access, verified.tokens.refresh)
  }

  return verified.subject
}

export async function login() {
  const cookies = await getCookies()
  const accessToken = cookies.get("access_token")
  const refreshToken = cookies.get("refresh_token")

  if (accessToken) {
    const verified = await client.verify(subjects, accessToken.value, {
      refresh: refreshToken?.value,
    })
    if (!verified.err && verified.tokens) {
      await setTokens(verified.tokens.access, verified.tokens.refresh)
      redirect("/")
    }
  }

  const headers = await getHeaders()
  const host = headers.get("host")
  const protocol = host?.includes("localhost") ? "http" : "https"
  const { url } = await client.authorize(
    `${protocol}://${host}/api/callback`,
    "code",
  )
  redirect(url)
}

export async function logout() {
  const cookies = await getCookies()
  cookies.delete("access_token")
  cookies.delete("refresh_token")

  redirect("/")
}


================================================
FILE: examples/quickstart/sst/app/api/callback/route.ts
================================================
import { client, setTokens } from "../../auth"
import { type NextRequest, NextResponse } from "next/server"

export async function GET(req: NextRequest) {
  const url = new URL(req.url)
  const code = url.searchParams.get("code")

  const exchanged = await client.exchange(code!, `${url.origin}/api/callback`)

  if (exchanged.err) return NextResponse.json(exchanged.err, { status: 400 })

  await setTokens(exchanged.tokens.access, exchanged.tokens.refresh)

  return NextResponse.redirect(`${url.origin}/`)
}


================================================
FILE: examples/quickstart/sst/app/auth.ts
================================================
import { Resource } from "sst"
import { createClient } from "@openauthjs/openauth/client"
import { cookies as getCookies } from "next/headers"

export const client = createClient({
  clientID: "nextjs",
  issuer: Resource.MyAuth.url,
})

export async function setTokens(access: string, refresh: string) {
  const cookies = await getCookies()

  cookies.set({
    name: "access_token",
    value: access,
    httpOnly: true,
    sameSite: "lax",
    path: "/",
    maxAge: 34560000,
  })
  cookies.set({
    name: "refresh_token",
    value: refresh,
    httpOnly: true,
    sameSite: "lax",
    path: "/",
    maxAge: 34560000,
  })
}


================================================
FILE: examples/quickstart/sst/app/globals.css
================================================
:root {
  --background: #ffffff;
  --foreground: #171717;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}

html,
body {
  max-width: 100vw;
  overflow-x: hidden;
}

body {
  color: var(--foreground);
  background: var(--background);
  font-family: Arial, Helvetica, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

@media (prefers-color-scheme: dark) {
  html {
    color-scheme: dark;
  }
}


================================================
FILE: examples/quickstart/sst/app/layout.tsx
================================================
import type { Metadata } from "next"
import { Geist, Geist_Mono } from "next/font/google"
import "./globals.css"

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
})

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
})

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable}`}>
        {children}
      </body>
    </html>
  )
}


================================================
FILE: examples/quickstart/sst/app/page.module.css
================================================
.page {
  --gray-rgb: 0, 0, 0;
  --gray-alpha-200: rgba(var(--gray-rgb), 0.08);
  --gray-alpha-100: rgba(var(--gray-rgb), 0.05);

  --button-primary-hover: #383838;
  --button-secondary-hover: #f2f2f2;

  display: grid;
  grid-template-rows: 20px 1fr 20px;
  align-items: center;
  justify-items: center;
  min-height: 100svh;
  padding: 80px;
  gap: 64px;
  font-family: var(--font-geist-sans);
}

@media (prefers-color-scheme: dark) {
  .page {
    --gray-rgb: 255, 255, 255;
    --gray-alpha-200: rgba(var(--gray-rgb), 0.145);
    --gray-alpha-100: rgba(var(--gray-rgb), 0.06);

    --button-primary-hover: #ccc;
    --button-secondary-hover: #1a1a1a;
  }
}

.main {
  display: flex;
  flex-direction: column;
  gap: 32px;
  grid-row-start: 2;
}

.main ol {
  font-family: var(--font-geist-mono);
  padding-left: 0;
  margin: 0;
  font-size: 14px;
  line-height: 24px;
  letter-spacing: -0.01em;
  list-style-position: inside;
}

.main li:not(:last-of-type) {
  margin-bottom: 8px;
}

.main code {
  font-family: inherit;
  background: var(--gray-alpha-100);
  padding: 2px 4px;
  border-radius: 4px;
  font-weight: 600;
}

.ctas {
  display: flex;
  gap: 16px;
}

.ctas a {
  appearance: none;
  border-radius: 128px;
  height: 48px;
  padding: 0 20px;
  border: none;
  border: 1px solid transparent;
  transition:
    background 0.2s,
    color 0.2s,
    border-color 0.2s;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  line-height: 20px;
  font-weight: 500;
}

a.primary {
  background: var(--foreground);
  color: var(--background);
  gap: 8px;
}

a.secondary {
  border-color: var(--gray-alpha-200);
  min-width: 180px;
}

.footer {
  grid-row-start: 3;
  display: flex;
  gap: 24px;
}

.footer a {
  display: flex;
  align-items: center;
  gap: 8px;
}

.footer img {
  flex-shrink: 0;
}

/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
  a.primary:hover {
    background: var(--button-primary-hover);
    border-color: transparent;
  }

  a.secondary:hover {
    background: var(--button-secondary-hover);
    border-color: transparent;
  }

  .footer a:hover {
    text-decoration: underline;
    text-underline-offset: 4px;
  }
}

@media (max-width: 600px) {
  .page {
    padding: 32px;
    padding-bottom: 80px;
  }

  .main {
    align-items: center;
  }

  .main ol {
    text-align: center;
  }

  .ctas {
    flex-direction: column;
  }

  .ctas a {
    font-size: 14px;
    height: 40px;
    padding: 0 16px;
  }

  a.secondary {
    min-width: auto;
  }

  .footer {
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
  }
}

@media (prefers-color-scheme: dark) {
  .logo {
    filter: invert();
  }
}

.ctas button {
  appearance: none;
  background: transparent;
  border-radius: 128px;
  height: 48px;
  padding: 0 20px;
  border: none;
  border: 1px solid transparent;
  transition:
    background 0.2s,
    color 0.2s,
    border-color 0.2s;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  line-height: 20px;
  font-weight: 500;
}

button.primary {
  background: var(--foreground);
  color: var(--background);
  gap: 8px;
}

button.secondary {
  border-color: var(--gray-alpha-200);
  min-width: 180px;
}


================================================
FILE: examples/quickstart/sst/app/page.tsx
================================================
import Image from "next/image"
import styles from "./page.module.css"
import { auth, login, logout } from "./actions"

export default async function Home() {
  const subject = await auth()

  return (
    <div className={styles.page}>
      <main className={styles.main}>
        <Image
          className={styles.logo}
          src="/next.svg"
          alt="Next.js logo"
          width={180}
          height={38}
          priority
        />
        <ol>
          {subject ? (
            <>
              <li>
                Logged in as <code>{subject.properties.id}</code>.
              </li>
              <li>
                And then check out <code>app/page.tsx</code>.
              </li>
            </>
          ) : (
            <>
              <li>Login with your email and password.</li>
              <li>
                And then check out <code>app/page.tsx</code>.
              </li>
            </>
          )}
        </ol>

        <div className={styles.ctas}>
          {subject ? (
            <form action={logout}>
              <button className={styles.secondary}>Logout</button>
            </form>
          ) : (
            <form action={login}>
              <button className={styles.primary}>Login with OpenAuth</button>
            </form>
          )}
        </div>
      </main>
    </div>
  )
}


================================================
FILE: examples/quickstart/sst/auth/index.ts
================================================
import { handle } from "hono/aws-lambda"
import { issuer } from "@openauthjs/openauth"
import { CodeUI } from "@openauthjs/openauth/ui/code"
import { CodeProvider } from "@openauthjs/openauth/provider/code"
import { MemoryStorage } from "@openauthjs/openauth/storage/memory"
import { subjects } from "./subjects"

async function getUser(email: string) {
  // Get user from database and return user ID
  return "123"
}

const app = issuer({
  subjects,
  storage: MemoryStorage(),
  // Remove after setting custom domain
  allow: async () => true,
  providers: {
    code: CodeProvider(
      CodeUI({
        sendCode: async (email, code) => {
          console.log(email, code)
        },
      }),
    ),
  },
  success: async (ctx, value) => {
    if (value.provider === "code") {
      return ctx.subject("user", {
        id: await getUser(value.claims.email),
      })
    }
    throw new Error("Invalid provider")
  },
})

export const handler = handle(app)


================================================
FILE: examples/quickstart/sst/auth/subjects.ts
================================================
import { object, string } from "valibot"
import { createSubjects } from "@openauthjs/openauth/subject"

export const subjects = createSubjects({
  user: object({
    id: string(),
  }),
})


================================================
FILE: examples/quickstart/sst/next.config.ts
================================================
import type { NextConfig } from "next"

const nextConfig: NextConfig = {
  /* config options here */
}

export default nextConfig


================================================
FILE: examples/quickstart/sst/package.json
================================================
{
  "name": "oa-nextjs",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "build": "next build",
    "dev": "next dev",
    "lint": "next lint",
    "start": "next start"
  },
  "dependencies": {
    "@openauthjs/openauth": "^0.3.2",
    "hono": "^4.6.16",
    "next": "15.1.4",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "sst": "latest",
    "valibot": "^1.0.0-beta.11"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "typescript": "^5"
  }
}


================================================
FILE: examples/quickstart/sst/sst-env.d.ts
================================================
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
import "sst"
export {}
declare module "sst" {
  export interface Resource {
    MyAuth: {
      type: "sst.aws.Auth"
      url: string
    }
    MyWeb: {
      type: "sst.aws.Nextjs"
      url: string
    }
  }
}


================================================
FILE: examples/quickstart/sst/sst.config.ts
================================================
/// <reference path="./.sst/platform/config.d.ts" />

export default $config({
  app(input) {
    return {
      name: "oa-nextjs",
      removal: input?.stage === "production" ? "retain" : "remove",
      protect: ["production"].includes(input?.stage),
      home: "aws",
    }
  },
  async run() {
    const auth = new sst.aws.Auth("MyAuth", {
      issuer: "auth/index.handler",
    })

    new sst.aws.Nextjs("MyWeb", {
      link: [auth],
    })
  },
})


================================================
FILE: examples/quickstart/sst/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules", "sst.config.ts"]
}


================================================
FILE: examples/quickstart/standalone/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts


================================================
FILE: examples/quickstart/standalone/README.md
================================================
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.


================================================
FILE: examples/quickstart/standalone/app/actions.ts
================================================
"use server"

import { redirect } from "next/navigation"
import { headers as getHeaders, cookies as getCookies } from "next/headers"
import { subjects } from "../auth/subjects"
import { client, setTokens } from "./auth"

export async function auth() {
  const cookies = await getCookies()
  const accessToken = cookies.get("access_token")
  const refreshToken = cookies.get("refresh_token")

  if (!accessToken) {
    return false
  }

  const verified = await client.verify(subjects, accessToken.value, {
    refresh: refreshToken?.value,
  })

  if (verified.err) {
    return false
  }
  if (verified.tokens) {
    await setTokens(verified.tokens.access, verified.tokens.refresh)
  }

  return verified.subject
}

export async function login() {
  const cookies = await getCookies()
  const accessToken = cookies.get("access_token")
  const refreshToken = cookies.get("refresh_token")

  if (accessToken) {
    const verified = await client.verify(subjects, accessToken.value, {
      refresh: refreshToken?.value,
    })
    if (!verified.err && verified.tokens) {
      await setTokens(verified.tokens.access, verified.tokens.refresh)
      redirect("/")
    }
  }

  const headers = await getHeaders()
  const host = headers.get("host")
  const protocol = host?.includes("localhost") ? "http" : "https"
  const { url } = await client.authorize(
    `${protocol}://${host}/api/callback`,
    "code",
  )
  redirect(url)
}

export async function logout() {
  const cookies = await getCookies()
  cookies.delete("access_token")
  cookies.delete("refresh_token")

  redirect("/")
}


================================================
FILE: examples/quickstart/standalone/app/api/callback/route.ts
================================================
import { client, setTokens } from "../../auth"
import { type NextRequest, NextResponse } from "next/server"

export async function GET(req: NextRequest) {
  const url = new URL(req.url)
  const code = url.searchParams.get("code")

  const exchanged = await client.exchange(code!, `${url.origin}/api/callback`)

  if (exchanged.err) return NextResponse.json(exchanged.err, { status: 400 })

  await setTokens(exchanged.tokens.access, exchanged.tokens.refresh)

  return NextResponse.redirect(`${url.origin}/`)
}


================================================
FILE: examples/quickstart/standalone/app/auth.ts
================================================
import { createClient } from "@openauthjs/openauth/client"
import { cookies as getCookies } from "next/headers"

export const client = createClient({
  clientID: "nextjs",
  issuer: "http://localhost:3001",
})

export async function setTokens(access: string, refresh: string) {
  const cookies = await getCookies()

  cookies.set({
    name: "access_token",
    value: access,
    httpOnly: true,
    sameSite: "lax",
    path: "/",
    maxAge: 34560000,
  })
  cookies.set({
    name: "refresh_token",
    value: refresh,
    httpOnly: true,
    sameSite: "lax",
    path: "/",
    maxAge: 34560000,
  })
}


================================================
FILE: examples/quickstart/standalone/app/globals.css
================================================
:root {
  --background: #ffffff;
  --foreground: #171717;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}

html,
body {
  max-width: 100vw;
  overflow-x: hidden;
}

body {
  color: var(--foreground);
  background: var(--background);
  font-family: Arial, Helvetica, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

@media (prefers-color-scheme: dark) {
  html {
    color-scheme: dark;
  }
}


================================================
FILE: examples/quickstart/standalone/app/layout.tsx
================================================
import type { Metadata } from "next"
import { Geist, Geist_Mono } from "next/font/google"
import "./globals.css"

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
})

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
})

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable}`}>
        {children}
      </body>
    </html>
  )
}


================================================
FILE: examples/quickstart/standalone/app/page.module.css
================================================
.page {
  --gray-rgb: 0, 0, 0;
  --gray-alpha-200: rgba(var(--gray-rgb), 0.08);
  --gray-alpha-100: rgba(var(--gray-rgb), 0.05);

  --button-primary-hover: #383838;
  --button-secondary-hover: #f2f2f2;

  display: grid;
  grid-template-rows: 20px 1fr 20px;
  align-items: center;
  justify-items: center;
  min-height: 100svh;
  padding: 80px;
  gap: 64px;
  font-family: var(--font-geist-sans);
}

@media (prefers-color-scheme: dark) {
  .page {
    --gray-rgb: 255, 255, 255;
    --gray-alpha-200: rgba(var(--gray-rgb), 0.145);
    --gray-alpha-100: rgba(var(--gray-rgb), 0.06);

    --button-primary-hover: #ccc;
    --button-secondary-hover: #1a1a1a;
  }
}

.main {
  display: flex;
  flex-direction: column;
  gap: 32px;
  grid-row-start: 2;
}

.main ol {
  font-family: var(--font-geist-mono);
  padding-left: 0;
  margin: 0;
  font-size: 14px;
  line-height: 24px;
  letter-spacing: -0.01em;
  list-style-position: inside;
}

.main li:not(:last-of-type) {
  margin-bottom: 8px;
}

.main code {
  font-family: inherit;
  background: var(--gray-alpha-100);
  padding: 2px 4px;
  border-radius: 4px;
  font-weight: 600;
}

.ctas {
  display: flex;
  gap: 16px;
}

.ctas a {
  appearance: none;
  border-radius: 128px;
  height: 48px;
  padding: 0 20px;
  border: none;
  border: 1px solid transparent;
  transition:
    background 0.2s,
    color 0.2s,
    border-color 0.2s;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  line-height: 20px;
  font-weight: 500;
}

a.primary {
  background: var(--foreground);
  color: var(--background);
  gap: 8px;
}

a.secondary {
  border-color: var(--gray-alpha-200);
  min-width: 180px;
}

.footer {
  grid-row-start: 3;
  display: flex;
  gap: 24px;
}

.footer a {
  display: flex;
  align-items: center;
  gap: 8px;
}

.footer img {
  flex-shrink: 0;
}

/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
  a.primary:hover {
    background: var(--button-primary-hover);
    border-color: transparent;
  }

  a.secondary:hover {
    background: var(--button-secondary-hover);
    border-color: transparent;
  }

  .footer a:hover {
    text-decoration: underline;
    text-underline-offset: 4px;
  }
}

@media (max-width: 600px) {
  .page {
    padding: 32px;
    padding-bottom: 80px;
  }

  .main {
    align-items: center;
  }

  .main ol {
    text-align: center;
  }

  .ctas {
    flex-direction: column;
  }

  .ctas a {
    font-size: 14px;
    height: 40px;
    padding: 0 16px;
  }

  a.secondary {
    min-width: auto;
  }

  .footer {
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
  }
}

@media (prefers-color-scheme: dark) {
  .logo {
    filter: invert();
  }
}

.ctas button {
  appearance: none;
  background: transparent;
  border-radius: 128px;
  height: 48px;
  padding: 0 20px;
  border: none;
  border: 1px solid transparent;
  transition:
    background 0.2s,
    color 0.2s,
    border-color 0.2s;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  line-height: 20px;
  font-weight: 500;
}

button.primary {
  background: var(--foreground);
  color: var(--background);
  gap: 8px;
}

button.secondary {
  border-color: var(--gray-alpha-200);
  min-width: 180px;
}


================================================
FILE: examples/quickstart/standalone/app/page.tsx
================================================
import Image from "next/image"
import styles from "./page.module.css"
import { auth, login, logout } from "./actions"

export default async function Home() {
  const subject = await auth()

  return (
    <div className={styles.page}>
      <main className={styles.main}>
        <Image
          className={styles.logo}
          src="/next.svg"
          alt="Next.js logo"
          width={180}
          height={38}
          priority
        />
        <ol>
          {subject ? (
            <>
              <li>
                Logged in as <code>{subject.properties.id}</code>.
              </li>
              <li>
                And then check out <code>app/page.tsx</code>.
              </li>
            </>
          ) : (
            <>
              <li>Login with your email and password.</li>
              <li>
                And then check out <code>app/page.tsx</code>.
              </li>
            </>
          )}
        </ol>

        <div className={styles.ctas}>
          {subject ? (
            <form action={logout}>
              <button className={styles.secondary}>Logout</button>
            </form>
          ) : (
            <form action={login}>
              <button className={styles.primary}>Login with OpenAuth</button>
            </form>
          )}
        </div>
      </main>
    </div>
  )
}


================================================
FILE: examples/quickstart/standalone/auth/index.ts
================================================
import { issuer } from "@openauthjs/openauth"
import { CodeUI } from "@openauthjs/openauth/ui/code"
import { CodeProvider } from "@openauthjs/openauth/provider/code"
import { MemoryStorage } from "@openauthjs/openauth/storage/memory"
import { subjects } from "./subjects"

async function getUser(email: string) {
  // Get user from database and return user ID
  return "123"
}

export default issuer({
  subjects,
  storage: MemoryStorage(),
  providers: {
    code: CodeProvider(
      CodeUI({
        sendCode: async (email, code) => {
          console.log(email, code)
        },
      }),
    ),
  },
  success: async (ctx, value) => {
    if (value.provider === "code") {
      return ctx.subject("user", {
        id: await getUser(value.claims.email),
      })
    }
    throw new Error("Invalid provider")
  },
})


================================================
FILE: examples/quickstart/standalone/auth/subjects.ts
================================================
import { object, string } from "valibot"
import { createSubjects } from "@openauthjs/openauth/subject"

export const subjects = createSubjects({
  user: object({
    id: string(),
  }),
})


================================================
FILE: examples/quickstart/standalone/next.config.ts
================================================
import type { NextConfig } from "next"

const nextConfig: NextConfig = {
  /* config options here */
}

export default nextConfig


================================================
FILE: examples/quickstart/standalone/package.json
================================================
{
  "name": "oa-nextjs",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "dev:auth": "PORT=3001 bun run --hot auth/index.ts",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@openauthjs/openauth": "^0.3.2",
    "next": "15.1.4",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "valibot": "^1.0.0-beta.11"
  },
  "devDependencies": {
    "typescript": "^5",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19"
  }
}


================================================
FILE: examples/quickstart/standalone/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}


================================================
FILE: examples/subjects.ts
================================================
import { object, string } from "valibot"
import { createSubjects } from "@openauthjs/openauth/subject"

export const subjects = createSubjects({
  user: object({
    id: string(),
  }),
})


================================================
FILE: examples/tsconfig.json
================================================
{
  "extends": "@tsconfig/node22/tsconfig.json",
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx"
  }
}


================================================
FILE: package.json
================================================
{
  "name": "openauthjs",
  "module": "index.ts",
  "type": "module",
  "workspaces": [
    "packages/openauth",
    "examples/issuer/*",
    "examples/client/*"
  ],
  "scripts": {
    "release": "bun run --filter=\"@openauthjs/openauth\" build && changeset publish"
  },
  "devDependencies": {
    "@tsconfig/node22": "22.0.0",
    "@types/bun": "latest"
  },
  "dependencies": {
    "@changesets/cli": "2.27.10",
    "prettier": "3.4.2",
    "typescript": "5.6.3"
  },
  "private": true
}


================================================
FILE: packages/openauth/CHANGELOG.md
================================================
# @openauthjs/openauth

## 0.4.3

### Patch Changes

- ec8ca65: include expires_in for refresh response

## 0.4.2

### Patch Changes

- a03e510: fix for fetch timeout, wrap everything in lazy

## 0.4.1

### Patch Changes

- 33959c3: better logging on oidc wellknown errors

## 0.4.0

### Minor Changes

- 4e38fa6: feat: Return expires_in from /token endpoint
- fcaafcf: Return signing alg from jwks.json endpoint

### Patch Changes

- 9e3c2ac: Call password validation callback on password reset
- dc40b02: Fix providers client id case from `clientId` to `clientID`

## 0.3.9

### Patch Changes

- 40f6033: enable logger by default
- 3ce40fd: log dynamo error cause

## 0.3.8

### Patch Changes

- c75005b: retry failed dynamo calls

## 0.3.7

### Patch Changes

- 9036544: Add PKCE option to Oauth2Provider
- 8f214e3: Import only hono type in util.ts
- 4cd9e96: add provider logos for apple, x, facebook, microsoft and slack
- 3e3c9e6: Add password validation callback
- f46946c: Add use: sig to jwks.
- 7d39e76: Add way to modify the dynamo ttl attribute name
- 754d776: Supports forwarded protocol and forwarded port in the relative URL
- 1b5525b: add ability to resend verification code during registration

## 0.3.6

### Patch Changes

- f7bd440: Adding a new default openauth theme

## 0.3.5

### Patch Changes

- b22fb30: fix: enable CORS on well-known routes

## 0.3.4

### Patch Changes

- 34ca2b0: remove catch all route so hono instance can be extended

## 0.3.3

### Patch Changes

- 9712422: fix: add charset meta tag to ui/base.tsx
- 92e7170: Adds support for refresh token reuse interval and reuse detection

  Also fixes an issue with token invalidation, where removing keys while scanning
  may cause some refresh tokens to be skipped (depending on storage provider.)

## 0.3.2

### Patch Changes

- 03da3e0: fix issue with oidc adapter

## 0.3.1

### Patch Changes

- 8764ed4: support specify custom subject

## 0.3.0

### Minor Changes

- b2af22a: renamed authorizer -> issuer and adapter -> provider

  this should be a superficial change, but it's a breaking change

  previously you imported adapters like this:

  ```js
  import { PasswordAdapter } from "@openauth/openauth/adapter/password"
  ```

  update it to this:

  ```js
  import { PasswordProvider } from "@openauth/openauth/provider/password"
  ```

  for the authorizer, you import it like this:

  ```js
  import { authorizer } from "@openauth/openauth"
  ```

  update it to this:

  ```js
  import { issuer } from "@openauth/openauth"
  ```

  also subjects should be imported deeply like this:

  ```js
  import { createSubjects } from "@openauth/openauth"
  ```

  update it to this:

  ```js
  import { createSubjects } from "@openauth/openauth/subject"
  ```

## 0.2.7

### Patch Changes

- 3004802: refactor: export `AuthorizationState` for better reusability
- 2975608: switching signing key algorithm to es256. generate seperate keys for symmetrical encryption. old keys will automatically be marked expired and not used
- c92604b: Adds support for a custom DynamoDB endpoint which enables use of a amazon/dynamodb-local container.

  Usabe example:

  ```ts
    storage: DynamoStorage({
      table: 'openauth-users',
      endpoint: 'http://localhost:8000',
    }),
  ```

## 0.2.6

### Patch Changes

- ca0df5d: ui: support phone mode for code ui
- d8d1580: Add slack adapter to the list of available adapters.
- ce44ed6: fix for password adapter not redirecting to the right place after change password flow
- 4940bef: fix: add `node:` prefix for built-in modules

## 0.2.5

### Patch Changes

- 8d6a243: fix: eliminate OTP bias and timing attack vulnerability
- 873d1af: support specifying granular ttl for access/refresh token

## 0.2.4

### Patch Changes

- 8b5f490: feat: Add copy customization to Code UI component

## 0.2.3

### Patch Changes

- 80238de: return aud field when verifying token

## 0.2.2

### Patch Changes

- 6da8647: fix copy for code resend

## 0.2.1

### Patch Changes

- 83125f1: Remove predefined scopes from Spotify adapter to allow user-defined scopes

## 0.2.0

### Minor Changes

- 8c3f050: BREAKING CHANGE: `client.exchange` and `client.authorize` signatures have changed.

  `client.exchange` will now return an `{ err, tokens }` object. check `if (result.err)` for errors.
  `client.authorize` now accepts `pkce: true` as an option. it is now async and returns a promise with `{ challenge, url}`. the `challenge` contains the `state` and `verifier` if using `pkce`

  all exchanges have been updated to reflect this if you would like to reference

### Patch Changes

- 0f93def: refactor: update storage adapters to use Date for expiry

## 0.1.2

### Patch Changes

- 584728f: Add common ColorScheme
- 41acdc2: ui: missing copy in password.tsx
- 2aa531b: Add GitHub Actions workflow for running tests

## 0.1.1

### Patch Changes

- 04cd031: if only single provider is configured, skip provider selection

## 0.1.0

### Minor Changes

- 3c8cdf8: BREAKING CHANGE:

  The api for `client` has changed. It no longer throws errors and instead returns an `err` field that you must check or ignore.

  All the examples have been updated to reflect this change.

## 0.0.26

### Patch Changes

- 5dd6aa4: feature: add twitter adapter

## 0.0.25

### Patch Changes

- 7e3fa38: feat(cognito): add CognitoAdapter
- f496e3a: Set input autocomplete attribute in password UI

## 0.0.24

### Patch Changes

- f695881: feature: added apple adapter

## 0.0.23

### Patch Changes

- a585875: remove console.log
- 079c514: feat: add JumpCloud

## 0.0.22

### Patch Changes

- d3391f4: do not import createClient from root - it causes some bundlers to include too much code

## 0.0.21

### Patch Changes

- acc2c5f: add tests for memory adapter and fixed issues with ttl
- 7630c87: added facebook, discord, and keycloak adapter

## 0.0.20

### Patch Changes

- 1a0ff69: fix for theme not being applied

## 0.0.19

### Patch Changes

- 0864481: allow configuring storage through environment

## 0.0.18

### Patch Changes

- bbf90c5: fix type issues when using ui components

## 0.0.17

### Patch Changes

- f43e320: test
- c10dfdd: test
- c10dfdd: test
- c10dfdd: test
- 2d81677: test changeset

## 0.0.16

### Patch Changes

- 515635f: rename package


================================================
FILE: packages/openauth/bunfig.toml
================================================
[test]
root = "./test"


================================================
FILE: packages/openauth/package.json
================================================
{
  "name": "@openauthjs/openauth",
  "version": "0.4.3",
  "type": "module",
  "scripts": {
    "build": "bun run script/build.ts",
    "test": "bun test"
  },
  "sideEffects": false,
  "devDependencies": {
    "@cloudflare/workers-types": "4.20241205.0",
    "@tsconfig/node22": "22.0.0",
    "@types/node": "22.10.1",
    "arctic": "2.2.2",
    "hono": "4.6.9",
    "typescript": "5.6.3",
    "valibot": "1.0.0-beta.15"
  },
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "types": "./dist/types/index.d.ts"
    },
    "./*": {
      "import": "./dist/esm/*.js",
      "types": "./dist/types/*.d.ts"
    },
    "./ui": {
      "import": "./dist/esm/ui/index.js",
      "types": "./dist/types/ui/index.d.ts"
    }
  },
  "peerDependencies": {
    "arctic": "^2.2.2",
    "hono": "^4.0.0"
  },
  "dependencies": {
    "@standard-schema/spec": "1.0.0-beta.3",
    "aws4fetch": "1.0.20",
    "jose": "5.9.6"
  },
  "files": [
    "src",
    "dist"
  ]
}


================================================
FILE: packages/openauth/script/build.ts
================================================
import { Glob, $ } from "bun"
import pkg from "../package.json"

await $`rm -rf dist`
const files = new Glob("./src/**/*.{ts,tsx}").scan()
for await (const file of files) {
  await Bun.build({
    format: "esm",
    outdir: "dist/esm",
    external: ["*"],
    root: "src",
    entrypoints: [file],
  })
}
await Bun.build({
  format: "esm",
  outdir: "dist/esm",
  external: [
    ...Object.keys(pkg.dependencies),
    ...Object.keys(pkg.peerDependencies),
  ],
  root: "src",
  entrypoints: ["./src/ui/base.tsx"],
})
await $`tsc --outDir dist/types --declaration --emitDeclarationOnly --declarationMap`


================================================
FILE: packages/openauth/src/client.ts
================================================
/**
 * Use the OpenAuth client kick off your OAuth flows, exchange tokens, refresh tokens,
 * and verify tokens.
 *
 * First, create a client.
 *
 * ```ts title="client.ts"
 * import { createClient } from "@openauthjs/openauth/client"
 *
 * const client = createClient({
 *   clientID: "my-client",
 *   issuer: "https://auth.myserver.com"
 * })
 * ```
 *
 * Kick off the OAuth flow by calling `authorize`.
 *
 * ```ts
 * const redirect_uri = "https://myserver.com/callback"
 *
 * const { url } = await client.authorize(
 *   redirect_uri,
 *   "code"
 * )
 * ```
 *
 * When the user completes the flow, `exchange` the code for tokens.
 *
 * ```ts
 * const tokens = await client.exchange(query.get("code"), redirect_uri)
 * ```
 *
 * And `verify` the tokens.
 *
 * ```ts
 * const verified = await client.verify(subjects, tokens.access)
 * ```
 *
 * @packageDocumentation
 */
import {
  createLocalJWKSet,
  errors,
  JSONWebKeySet,
  jwtVerify,
  decodeJwt,
} from "jose"
import { SubjectSchema } from "./subject.js"
import type { v1 } from "@standard-schema/spec"
import {
  InvalidAccessTokenError,
  InvalidAuthorizationCodeError,
  InvalidRefreshTokenError,
  InvalidSubjectError,
} from "./error.js"
import { generatePKCE } from "./pkce.js"

/**
 * The well-known information for an OAuth 2.0 authorization server.
 * @internal
 */
export interface WellKnown {
  /**
   * The URI to the JWKS endpoint.
   */
  jwks_uri: string
  /**
   * The URI to the token endpoint.
   */
  token_endpoint: string
  /**
   * The URI to the authorization endpoint.
   */
  authorization_endpoint: string
}

/**
 * The tokens returned by the auth server.
 */
export interface Tokens {
  /**
   * The access token.
   */
  access: string
  /**
   * The refresh token.
   */
  refresh: string

  /**
   * The number of seconds until the access token expires.
   */
  expiresIn: number
}

interface ResponseLike {
  json(): Promise<unknown>
  ok: Response["ok"]
}
type FetchLike = (...args: any[]) => Promise<ResponseLike>

/**
 * The challenge that you can use to verify the code.
 */
export type Challenge = {
  /**
   * The state that was sent to the redirect URI.
   */
  state: string
  /**
   * The verifier that was sent to the redirect URI.
   */
  verifier?: string
}

/**
 * Configure the client.
 */
export interface ClientInput {
  /**
   * The client ID. This is just a string to identify your app.
   *
   * If you have a web app and a mobile app, you want to use different client IDs both.
   *
   * @example
   * ```ts
   * {
   *   clientID: "my-client"
   * }
   * ```
   */
  clientID: string
  /**
   * The URL of your OpenAuth server.
   *
   * @example
   * ```ts
   * {
   *   issuer: "https://auth.myserver.com"
   * }
   * ```
   */
  issuer?: string
  /**
   * Optionally, override the internally used fetch function.
   *
   * This is useful if you are using a polyfilled fetch function in your application and you
   * want the client to use it too.
   */
  fetch?: FetchLike
}

export interface AuthorizeOptions {
  /**
   * Enable the PKCE flow. This is for SPA apps.
   *
   * ```ts
   * {
   *   pkce: true
   * }
   * ```
   *
   * @default false
   */
  pkce?: boolean
  /**
   * The provider you want to use for the OAuth flow.
   *
   * ```ts
   * {
   *   provider: "google"
   * }
   * ```
   *
   * If no provider is specified, the user is directed to a page where they can select from the
   * list of configured providers.
   *
   * If there's only one provider configured, the user will be redirected to that.
   */
  provider?: string
}

export interface AuthorizeResult {
  /**
   * The challenge that you can use to verify the code. This is for the PKCE flow for SPA apps.
   *
   * This is an object that you _stringify_ and store it in session storage.
   *
   * ```ts
   * sessionStorage.setItem("challenge", JSON.stringify(challenge))
   * ```
   */
  challenge: Challenge
  /**
   * The URL to redirect the user to. This starts the OAuth flow.
   *
   * For example, for SPA apps.
   *
   * ```ts
   * location.href = url
   * ```
   */
  url: string
}

/**
 * Returned when the exchange is successful.
 */
export interface ExchangeSuccess {
  /**
   * This is always `false` when the exchange is successful.
   */
  err: false
  /**
   * The access and refresh tokens.
   */
  tokens: Tokens
}

/**
 * Returned when the exchange fails.
 */
export interface ExchangeError {
  /**
   * The type of error that occurred. You can handle this by checking the type.
   *
   * @example
   * ```ts
   * import { InvalidAuthorizationCodeError } from "@openauthjs/openauth/error"
   *
   * console.log(err instanceof InvalidAuthorizationCodeError)
   *```
   */
  err: InvalidAuthorizationCodeError
}

export interface RefreshOptions {
  /**
   * Optionally, pass in the access token.
   */
  access?: string
}

/**
 * Returned when the refresh is successful.
 */
export interface RefreshSuccess {
  /**
   * This is always `false` when the refresh is successful.
   */
  err: false
  /**
   * Returns the refreshed tokens only if they've been refreshed.
   *
   * If they are still valid, this will be `undefined`.
   */
  tokens?: Tokens
}

/**
 * Returned when the refresh fails.
 */
export interface RefreshError {
  /**
   * The type of error that occurred. You can handle this by checking the type.
   *
   * @example
   * ```ts
   * import { InvalidRefreshTokenError } from "@openauthjs/openauth/error"
   *
   * console.log(err instanceof InvalidRefreshTokenError)
   *```
   */
  err: InvalidRefreshTokenError | InvalidAccessTokenError
}

export interface VerifyOptions {
  /**
   * Optionally, pass in the refresh token.
   *
   * If passed in, this will automatically refresh the access token if it has expired.
   */
  refresh?: string
  /**
   * @internal
   */
  issuer?: string
  /**
   * @internal
   */
  audience?: string
  /**
   * Optionally, override the internally used fetch function.
   *
   * This is useful if you are using a polyfilled fetch function in your application and you
   * want the client to use it too.
   */
  fetch?: FetchLike
}

export interface VerifyResult<T extends SubjectSchema> {
  /**
   * This is always `undefined` when the verify is successful.
   */
  err?: undefined
  /**
   * Returns the refreshed tokens only if they’ve been refreshed.
   *
   * If they are still valid, this will be undefined.
   */
  tokens?: Tokens
  /**
   * @internal
   */
  aud: string
  /**
   * The decoded subjects from the access token.
   *
   * Has the same shape as the subjects you defined when creating the issuer.
   */
  subject: {
    [type in keyof T]: { type: type; properties: v1.InferOutput<T[type]> }
  }[keyof T]
}

/**
 * Returned when the verify call fails.
 */
export interface VerifyError {
  /**
   * The type of error that occurred. You can handle this by checking the type.
   *
   * @example
   * ```ts
   * import { InvalidRefreshTokenError } from "@openauthjs/openauth/error"
   *
   * console.log(err instanceof InvalidRefreshTokenError)
   *```
   */
  err: InvalidRefreshTokenError | InvalidAccessTokenError
}

/**
 * An instance of the OpenAuth client contains the following methods.
 */
export interface Client {
  /**
   * Start the autorization flow. For example, in SSR sites.
   *
   * ```ts
   * const { url } = await client.authorize(<redirect_uri>, "code")
   * ```
   *
   * This takes a redirect URI and the type of flow you want to use. The redirect URI is the
   * location where the user will be redirected to after the flow is complete.
   *
   * Supports both the _code_ and _token_ flows. We recommend using the _code_ flow as it's more
   * secure.
   *
   * :::tip
   * This returns a URL to redirect the user to. This starts the OAuth flow.
   * :::
   *
   * This returns a URL to the auth server. You can redirect the user to the URL to start the
   * OAuth flow.
   *
   * For SPA apps, we recommend using the PKCE flow.
   *
   * ```ts {4}
   * const { challenge, url } = await client.authorize(
   *   <redirect_uri>,
   *   "code",
   *   { pkce: true }
   * )
   * ```
   *
   * This returns a redirect URL and a challenge that you need to use later to verify the code.
   */
  authorize(
    redirectURI: string,
    response: "code" | "token",
    opts?: AuthorizeOptions,
  ): Promise<AuthorizeResult>
  /**
   * Exchange the code for access and refresh tokens.
   *
   * ```ts
   * const exchanged = await client.exchange(<code>, <redirect_uri>)
   * ```
   *
   * You call this after the user has been redirected back to your app after the OAuth flow.
   *
   * :::tip
   * For SSR sites, the code is returned in the query parameter.
   * :::
   *
   * So the code comes from the query parameter in the redirect URI. The redirect URI here is
   * the one that you passed in to the `authorize` call when starting the flow.
   *
   * :::tip
   * For SPA sites, the code is returned through the URL hash.
   * :::
   *
   * If you used the PKCE flow for an SPA app, the code is returned as a part of the redirect URL
   * hash.
   *
   * ```ts {4}
   * const exchanged = await client.exchange(
   *   <code>,
   *   <redirect_uri>,
   *   <challenge.verifier>
   * )
   * ```
   *
   * You also need to pass in the previously stored challenge verifier.
   *
   * This method returns the access and refresh tokens. Or if it fails, it returns an error that
   * you can handle depending on the error.
   *
   * ```ts
   * import { InvalidAuthorizationCodeError } from "@openauthjs/openauth/error"
   *
   * if (exchanged.err) {
   *   if (exchanged.err instanceof InvalidAuthorizationCodeError) {
   *     // handle invalid code error
   *   }
   *   else {
   *     // handle other errors
   *   }
   * }
   *
   * const { access, refresh } = exchanged.tokens
   * ```
   */
  exchange(
    code: string,
    redirectURI: string,
    verifier?: string,
  ): Promise<ExchangeSuccess | ExchangeError>
  /**
   * Refreshes the tokens if they have expired. This is used in an SPA app to maintain the
   * session, without logging the user out.
   *
   * ```ts
   * const next = await client.refresh(<refresh_token>)
   * ```
   *
   * Can optionally take the access token as well. If passed in, this will skip the refresh
   * if the access token is still valid.
   *
   * ```ts
   * const next = await client.refresh(<refresh_token>, { access: <access_token> })
   * ```
   *
   * This returns the refreshed tokens only if they've been refreshed.
   *
   * ```ts
   * if (!next.err) {
   *   // tokens are still valid
   * }
   * if (next.tokens) {
   *   const { access, refresh } = next.tokens
   * }
   * ```
   *
   * Or if it fails, it returns an error that you can handle depending on the error.
   *
   * ```ts
   * import { InvalidRefreshTokenError } from "@openauthjs/openauth/error"
   *
   * if (next.err) {
   *   if (next.err instanceof InvalidRefreshTokenError) {
   *     // handle invalid refresh token error
   *   }
   *   else {
   *     // handle other errors
   *   }
   * }
   * ```
   */
  refresh(
    refresh: string,
    opts?: RefreshOptions,
  ): Promise<RefreshSuccess | RefreshError>
  /**
   * Verify the token in the incoming request.
   *
   * This is typically used for SSR sites where the token is stored in an HTTP only cookie. And
   * is passed to the server on every request.
   *
   * ```ts
   * const verified = await client.verify(<subjects>, <token>)
   * ```
   *
   * This takes the subjects that you had previously defined when creating the issuer.
   *
   * :::tip
   * If the refresh token is passed in, it'll automatically refresh the access token.
   * :::
   *
   * This can optionally take the refresh token as well. If passed in, it'll automatically
   * refresh the access token if it has expired.
   *
   * ```ts
   * const verified = await client.verify(<subjects>, <token>, { refresh: <refresh_token> })
   * ```
   *
   * This returns the decoded subjects from the access token. And the tokens if they've been
   * refreshed.
   *
   * ```ts
   * // based on the subjects you defined earlier
   * console.log(verified.subject.properties.userID)
   *
   * if (verified.tokens) {
   *   const { access, refresh } = verified.tokens
   * }
   * ```
   *
   * Or if it fails, it returns an error that you can handle depending on the error.
   *
   * ```ts
   * import { InvalidRefreshTokenError } from "@openauthjs/openauth/error"
   *
   * if (verified.err) {
   *   if (verified.err instanceof InvalidRefreshTokenError) {
   *     // handle invalid refresh token error
   *   }
   *   else {
   *     // handle other errors
   *   }
   * }
   * ```
   */
  verify<T extends SubjectSchema>(
    subjects: T,
    token: string,
    options?: VerifyOptions,
  ): Promise<VerifyResult<T> | VerifyError>
}

/**
 * Create an OpenAuth client.
 *
 * @param input - Configure the client.
 */
export function createClient(input: ClientInput): Client {
  const jwksCache = new Map<string, ReturnType<typeof createLocalJWKSet>>()
  const issuerCache = new Map<string, WellKnown>()
  const issuer = input.issuer || process.env.OPENAUTH_ISSUER
  if (!issuer) throw new Error("No issuer")
  const f = input.fetch ?? fetch

  async function getIssuer() {
    const cached = issuerCache.get(issuer!)
    if (cached) return cached
    const wellKnown = (await (f || fetch)(
      `${issuer}/.well-known/oauth-authorization-server`,
    ).then((r) => r.json())) as WellKnown
    issuerCache.set(issuer!, wellKnown)
    return wellKnown
  }

  async function getJWKS() {
    const wk = await getIssuer()
    const cached = jwksCache.get(issuer!)
    if (cached) return cached
    const keyset = (await (f || fetch)(wk.jwks_uri).then((r) =>
      r.json(),
    )) as JSONWebKeySet
    const result = createLocalJWKSet(keyset)
    jwksCache.set(issuer!, result)
    return result
  }

  const result = {
    async authorize(
      redirectURI: string,
      response: "code" | "token",
      opts?: AuthorizeOptions,
    ) {
      const result = new URL(issuer + "/authorize")
      const challenge: Challenge = {
        state: crypto.randomUUID(),
      }
      result.searchParams.set("client_id", input.clientID)
      result.searchParams.set("redirect_uri", redirectURI)
      result.searchParams.set("response_type", response)
      result.searchParams.set("state", challenge.state)
      if (opts?.provider) result.searchParams.set("provider", opts.provider)
      if (opts?.pkce && response === "code") {
        const pkce = await generatePKCE()
        result.searchParams.set("code_challenge_method", "S256")
        result.searchParams.set("code_challenge", pkce.challenge)
        challenge.verifier = pkce.verifier
      }
      return {
        challenge,
        url: result.toString(),
      }
    },
    /**
     * @deprecated use `authorize` instead, it will do pkce by default unless disabled with `opts.pkce = false`
     */
    async pkce(
      redirectURI: string,
      opts?: {
        provider?: string
      },
    ) {
      const result = new URL(issuer + "/authorize")
      if (opts?.provider) result.searchParams.set("provider", opts.provider)
      result.searchParams.set("client_id", input.clientID)
      result.searchParams.set("redirect_uri", redirectURI)
      result.searchParams.set("response_type", "code")
      const pkce = await generatePKCE()
      result.searchParams.set("code_challenge_method", "S256")
      result.searchParams.set("code_challenge", pkce.challenge)
      return [pkce.verifier, result.toString()]
    },
    async exchange(
      code: string,
      redirectURI: string,
      verifier?: string,
    ): Promise<ExchangeSuccess | ExchangeError> {
      const tokens = await f(issuer + "/token", {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: new URLSearchParams({
          code,
          redirect_uri: redirectURI,
          grant_type: "authorization_code",
          client_id: input.clientID,
          code_verifier: verifier || "",
        }).toString(),
      })
      const json = (await tokens.json()) as any
      if (!tokens.ok) {
        return {
          err: new InvalidAuthorizationCodeError(),
        }
      }
      return {
        err: false,
        tokens: {
          access: json.access_token as string,
          refresh: json.refresh_token as string,
          expiresIn: json.expires_in as number,
        },
      }
    },
    async refresh(
      refresh: string,
      opts?: RefreshOptions,
    ): Promise<RefreshSuccess | RefreshError> {
      if (opts && opts.access) {
        const decoded = decodeJwt(opts.access)
        if (!decoded) {
          return {
            err: new InvalidAccessTokenError(),
          }
        }
        // allow 30s window for expiration
        if ((decoded.exp || 0) > Date.now() / 1000 + 30) {
          return {
            err: false,
          }
        }
      }
      const tokens = await f(issuer + "/token", {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: new URLSearchParams({
          grant_type: "refresh_token",
          refresh_token: refresh,
        }).toString(),
      })
      const json = (await tokens.json()) as any
      if (!tokens.ok) {
        return {
          err: new InvalidRefreshTokenError(),
        }
      }
      return {
        err: false,
        tokens: {
          access: json.access_token as string,
          refresh: json.refresh_token as string,
          expiresIn: json.expires_in as number,
        },
      }
    },
    async verify<T extends SubjectSchema>(
      subjects: T,
      token: string,
      options?: VerifyOptions,
    ): Promise<VerifyResult<T> | VerifyError> {
      const jwks = await getJWKS()
      try {
        const result = await jwtVerify<{
          mode: "access"
          type: keyof T
          properties: v1.InferInput<T[keyof T]>
        }>(token, jwks, {
          issuer,
        })
        const validated = await subjects[result.payload.type][
          "~standard"
        ].validate(result.payload.properties)
        if (!validated.issues && result.payload.mode === "access")
          return {
            aud: result.payload.aud as string,
            subject: {
              type: result.payload.type,
              properties: validated.value,
            } as any,
          }
        return {
          err: new InvalidSubjectError(),
        }
      } catch (e) {
        if (e instanceof errors.JWTExpired && options?.refresh) {
          const refreshed = await this.refresh(options.refresh)
          if (refreshed.err) return refreshed
          const verified = await result.verify(
            subjects,
            refreshed.tokens!.access,
            {
              refresh: refreshed.tokens!.refresh,
              issuer,
              fetch: options?.fetch,
            },
          )
          if (verified.err) return verified
          verified.tokens = refreshed.tokens
          return verified
        }
        return {
          err: new InvalidAccessTokenError(),
        }
      }
    },
  }
  return result
}


================================================
FILE: packages/openauth/src/css.d.ts
================================================
declare module "*.css" {
  const css: string
  export default css
}


================================================
FILE: packages/openauth/src/error.ts
================================================
/**
 * A list of errors that can be thrown by OpenAuth.
 *
 * You can use these errors to check the type of error and handle it. For example.
 *
 * ```ts
 * import { InvalidAuthorizationCodeError } from "@openauthjs/openauth/error"
 *
 * if (err instanceof InvalidAuthorizationCodeError) {
 *   // handle invalid code error
 * }
 * ```
 *
 * @packageDocumentation
 */

/**
 * The OAuth server returned an error.
 */
export class OauthError extends Error {
  constructor(
    public error:
      | "invalid_request"
      | "invalid_grant"
      | "unauthorized_client"
      | "access_denied"
      | "unsupported_grant_type"
      | "server_error"
      | "temporarily_unavailable",
    public description: string,
  ) {
    super(error + " - " + description)
  }
}

/**
 * The `provider` needs to be passed in.
 */
export class MissingProviderError extends OauthError {
  constructor() {
    super(
      "invalid_request",
      "Must specify `provider` query parameter if `select` callback on issuer is not specified",
    )
  }
}

/**
 * The given parameter is missing.
 */
export class MissingParameterError extends OauthError {
  constructor(public parameter: string) {
    super("invalid_request", "Missing parameter: " + parameter)
  }
}

/**
 * The given client is not authorized to use the redirect URI that was passed in.
 */
export class UnauthorizedClientError extends OauthError {
  constructor(
    public clientID: string,
    redirectURI: string,
  ) {
    super(
      "unauthorized_client",
      `Client ${clientID} is not authorized to use this redirect_uri: ${redirectURI}`,
    )
  }
}

/**
 * The browser was in an unknown state.
 *
 * This can happen when certain cookies have expired. Or the browser was switched in the middle
 * of the authentication flow.
 */
export class UnknownStateError extends Error {
  constructor() {
    super(
      "The browser was in an unknown state. This could be because certain cookies expired or the browser was switched in the middle of an authentication flow.",
    )
  }
}

/**
 * The given subject is invalid.
 */
export class InvalidSubjectError extends Error {
  constructor() {
    super("Invalid subject")
  }
}

/**
 * The given refresh token is invalid.
 */
export class InvalidRefreshTokenError extends Error {
  constructor() {
    super("Invalid refresh token")
  }
}

/**
 * The given access token is invalid.
 */
export class InvalidAccessTokenError extends Error {
  constructor() {
    super("Invalid access token")
  }
}

/**
 * The given authorization code is invalid.
 */
export class InvalidAuthorizationCodeError extends Error {
  constructor() {
    super("Invalid authorization code")
  }
}


================================================
FILE: packages/openauth/src/index.ts
================================================
export {
  /**
   * @deprecated
   * Use `import { createClient } from "@openauthjs/openauth/client"` instead - it will tree shake better
   */
  createClient,
} from "./client.js"

export {
  /**
   * @deprecated
   * Use `import { createSubjects } from "@openauthjs/openauth/subject"` instead - it will tree shake better
   */
  createSubjects,
} from "./subject.js"

import { issuer } from "./issuer.js"

export {
  /**
   * @deprecated
   * Use `import { issuer } from "@openauthjs/openauth"` instead, it was renamed
   */
  issuer as authorizer,
  issuer,
}


================================================
FILE: packages/openauth/src/issuer.ts
================================================
/**
 * The `issuer` create an OpentAuth server, a [Hono](https://hono.dev) app that's
 * designed to run anywhere.
 *
 * The `issuer` function requires a few things:
 *
 * ```ts title="issuer.ts"
 * import { issuer } from "@openauthjs/openauth"
 *
 * const app = issuer({
 *   providers: { ... },
 *   storage,
 *   subjects,
 *   success: async (ctx, value) => { ... }
 * })
 * ```
 *
 * #### Add providers
 *
 * You start by specifying the auth providers you are going to use. Let's say you want your users
 * to be able to authenticate with GitHub and with their email and password.
 *
 * ```ts title="issuer.ts"
 * import { GithubProvider } from "@openauthjs/openauth/provider/github"
 * import { PasswordProvider } from "@openauthjs/openauth/provider/password"
 *
 * const app = issuer({
 *   providers: {
 *     github: GithubProvider({
 *       // ...
 *     }),
 *     password: PasswordProvider({
 *       // ...
 *     }),
 *   },
 * })
 * ```
 *
 * #### Handle success
 *
 * The `success` callback receives the payload when a user completes a provider's auth flow.
 *
 * ```ts title="issuer.ts"
 * const app = issuer({
 *   providers: { ... },
 *   subjects,
 *   async success(ctx, value) {
 *     let userID
 *     if (value.provider === "password") {
 *       console.log(value.email)
 *       userID = ... // lookup user or create them
 *     }
 *     if (value.provider === "github") {
 *       console.log(value.tokenset.access)
 *       userID = ... // lookup user or create them
 *     }
 *     return ctx.subject("user", {
 *       userID
 *     })
 *   }
 * })
 * ```
 *
 * Once complete, the `issuer` issues the access tokens that a client can use. The `ctx.subject`
 * call is what is placed in the access token as a JWT.
 *
 * #### Define subjects
 *
 * You define the shape of these in the `subjects` field.
 *
 * ```ts title="subjects.ts"
 * import { object, string } from "valibot"
 * import { createSubjects } from "@openauthjs/openauth/subject"
 *
 * const subjects = createSubjects({
 *   user: object({
 *     userID: string()
 *   })
 * })
 * ```
 *
 * It's good to place this in a separate file since this'll be used in your client apps as well.
 *
 * ```ts title="issuer.ts"
 * import { subjects } from "./subjects.js"
 *
 * const app = issuer({
 *   providers: { ... },
 *   subjects,
 *   // ...
 * })
 * ```
 *
 * #### Deploy
 *
 * Since `issuer` is a Hono app, you can deploy it anywhere Hono supports.
 *
 * <Tabs>
 *   <TabItem label="Node">
 *   ```ts title="issuer.ts"
 *   import { serve } from "@hono/node-server"
 *
 *   serve(app)
 *   ```
 *   </TabItem>
 *   <TabItem label="Lambda">
 *   ```ts title="issuer.ts"
 *   import { handle } from "hono/aws-lambda"
 *
 *   export const handler = handle(app)
 *   ```
 *   </TabItem>
 *   <TabItem label="Bun">
 *   ```ts title="issuer.ts"
 *   export default app
 *   ```
 *   </TabItem>
 *   <TabItem label="Workers">
 *   ```ts title="issuer.ts"
 *   export default app
 *   ```
 *   </TabItem>
 * </Tabs>
 *
 * @packageDocumentation
 */
import { Provider, ProviderOptions } from "./provider/provider.js"
import { SubjectPayload, SubjectSchema } from "./subject.js"
import { Hono } from "hono/tiny"
import { handle as awsHandle } from "hono/aws-lambda"
import { Context } from "hono"
import { deleteCookie, getCookie, setCookie } from "hono/cookie"
import type { v1 } from "@standard-schema/spec"

/**
 * Sets the subject payload in the JWT token and returns the response.
 *
 * ```ts
 * ctx.subject("user", {
 *   userID
 * })
 * ```
 */
export interface OnSuccessResponder<
  T extends { type: string; properties: any },
> {
  /**
   * The `type` is the type of the subject, that was defined in the `subjects` field.
   *
   * The `properties` are the properties of the subject. This is the shape of the subject that
   * you defined in the `subjects` field.
   */
  subject<Type extends T["type"]>(
    type: Type,
    properties: Extract<T, { type: Type }>["properties"],
    opts?: {
      ttl?: {
        access?: number
        refresh?: number
      }
      subject?: string
    },
  ): Promise<Response>
}

/**
 * @internal
 */
export interface AuthorizationState {
  redirect_uri: string
  response_type: string
  state: string
  client_id: string
  audience?: string
  pkce?: {
    challenge: string
    method: "S256"
  }
}

/**
 * @internal
 */
export type Prettify<T> = {
  [K in keyof T]: T[K]
} & {}

import {
  MissingParameterError,
  OauthError,
  UnauthorizedClientError,
  UnknownStateError,
} from "./error.js"
import { compactDecrypt, CompactEncrypt, jwtVerify, SignJWT } from "jose"
import { Storage, StorageAdapter } from "./storage/storage.js"
import { encryptionKeys, legacySigningKeys, signingKeys } from "./keys.js"
import { validatePKCE } from "./pkce.js"
import { Select } from "./ui/select.js"
import { setTheme, Theme } from "./ui/theme.js"
import { getRelativeUrl, isDomainMatch, lazy } from "./util.js"
import { DynamoStorage } from "./storage/dynamo.js"
import { MemoryStorage } from "./storage/memory.js"
import { cors } from "hono/cors"
import { logger } from "hono/logger"

/** @internal */
export const aws = awsHandle

export interface IssuerInput<
  Providers extends Record<string, Provider<any>>,
  Subjects extends SubjectSchema,
  Result = {
    [key in keyof Providers]: Prettify<
      {
        provider: key
      } & (Providers[key] extends Provider<infer T> ? T : {})
    >
  }[keyof Providers],
> {
  /**
   * The shape of the subjects that you want to return.
   *
   * @example
   *
   * ```ts title="issuer.ts"
   * import { object, string } from "valibot"
   * import { createSubjects } from "@openauthjs/openauth/subject"
   *
   * issuer({
   *   subjects: createSubjects({
   *     user: object({
   *       userID: string()
   *     })
   *   })
   *   // ...
   * })
   * ```
   */
  subjects: Subjects
  /**
   * The storage adapter that you want to use.
   *
   * @example
   * ```ts title="issuer.ts"
   * import { DynamoStorage } from "@openauthjs/openauth/storage/dynamo"
   *
   * issuer({
   *   storage: DynamoStorage()
   *   // ...
   * })
   * ```
   */
  storage?: StorageAdapter
  /**
   * The providers that you want your OpenAuth server to support.
   *
   * @example
   *
   * ```ts title="issuer.ts"
   * import { GithubProvider } from "@openauthjs/openauth/provider/github"
   *
   * issuer({
   *   providers: {
   *     github: GithubProvider()
   *   }
   * })
   * ```
   *
   * The key is just a string that you can use to identify the provider. It's passed back to
   * the `success` callback.
   *
   * You can also specify multiple providers.
   *
   * ```ts
   * {
   *   providers: {
   *     github: GithubProvider(),
   *     google: GoogleProvider()
   *   }
   * }
   * ```
   */
  providers: Providers
  /**
   * The theme you want to use for the UI.
   *
   * This includes the UI the user sees when selecting a provider. And the `PasswordUI` and
   * `CodeUI` that are used by the `PasswordProvider` and `CodeProvider`.
   *
   * @example
   * ```ts title="issuer.ts"
   * import { THEME_SST } from "@openauthjs/openauth/ui/theme"
   *
   * issuer({
   *   theme: THEME_SST
   *   // ...
   * })
   * ```
   *
   * Or define your own.
   *
   * ```ts title="issuer.ts"
   * import type { Theme } from "@openauthjs/openauth/ui/theme"
   *
   * const MY_THEME: Theme = {
   *   // ...
   * }
   *
   * issuer({
   *   theme: MY_THEME
   *   // ...
   * })
   * ```
   */
  theme?: Theme
  /**
   * Set the TTL, in seconds, for access and refresh tokens.
   *
   * @example
   * ```ts
   * {
   *   ttl: {
   *     access: 60 * 60 * 24 * 30,
   *     refresh: 60 * 60 * 24 * 365
   *   }
   * }
   * ```
   */
  ttl?: {
    /**
     * Interval in seconds where the access token is valid.
     * @default 30d
     */
    access?: number
    /**
     * Interval in seconds where the refresh token is valid.
     * @default 1y
     */
    refresh?: number
    /**
     * Interval in seconds where refresh token reuse is allowed. This helps mitigrate
     * concurrency issues.
     * @default 60s
     */
    reuse?: number
    /**
     * Interval in seconds to retain refresh tokens for reuse detection.
     * @default 0s
     */
    retention?: number
  }
  /**
   * Optionally, configure the UI that's displayed when the user visits the root URL of the
   * of the OpenAuth server.
   *
   * ```ts title="issuer.ts"
   * import { Select } from "@openauthjs/openauth/ui/select"
   *
   * issuer({
   *   select: Select({
   *     providers: {
   *       github: { hide: true },
   *       google: { display: "Google" }
   *     }
   *   })
   *   // ...
   * })
   * ```
   *
   * @default Select()
   */
  select?(providers: Record<string, string>, req: Request): Promise<Response>
  /**
   * @internal
   */
  start?(req: Request): Promise<void>
  /**
   * The success callback that's called when the user completes the flow.
   *
   * This is called after the user has been redirected back to your app after the OAuth flow.
   *
   * @example
   * ```ts
   * {
   *   success: async (ctx, value) => {
   *     let userID
   *     if (value.provider === "password") {
   *       console.log(value.email)
   *       userID = ... // lookup user or create them
   *     }
   *     if (value.provider === "github") {
   *       console.log(value.tokenset.access)
   *       userID = ... // lookup user or create them
   *     }
   *     return ctx.subject("user", {
   *       userID
   *     })
   *   },
   *   // ...
   * }
   * ```
   */
  success(
    response: OnSuccessResponder<SubjectPayload<Subjects>>,
    input: Result,
    req: Request,
  ): Promise<Response>
  /**
   * @internal
   */
  error?(error: UnknownStateError, req: Request): Promise<Response>
  /**
   * Override the logic for whether a client request is allowed to call the issuer.
   *
   * By default, it uses the following:
   *
   * - Allow if the `redirectURI` is localhost.
   * - Compare `redirectURI` to the request's hostname or the `x-forwarded-host` header. If they
   *   are from the same sub-domain level, then allow.
   *
   * @example
   * ```ts
   * {
   *   allow: async (input, req) => {
   *     // Allow all clients
   *     return true
   *   }
   * }
   * ```
   */
  allow?(
    input: {
      clientID: string
      redirectURI: string
      audience?: string
    },
    req: Request,
  ): Promise<boolean>
}

/**
 * Create an OpenAuth server, a Hono app.
 */
export function issuer<
  Providers extends Record<string, Provider<any>>,
  Subjects extends SubjectSchema,
  Result = {
    [key in keyof Providers]: Prettify<
      {
        provider: key
      } & (Providers[key] extends Provider<infer T> ? T : {})
    >
  }[keyof Providers],
>(input: IssuerInput<Providers, Subjects, Result>) {
  const error =
    input.error ??
    function (err) {
      return new Response(err.message, {
        status: 400,
        headers: {
          "Content-Type": "text/plain",
        },
      })
    }
  const ttlAccess = input.ttl?.access ?? 60 * 60 * 24 * 30
  const ttlRefresh = input.ttl?.refresh ?? 60 * 60 * 24 * 365
  const ttlRefreshReuse = input.ttl?.reuse ?? 60
  const ttlRefreshRetention = input.ttl?.retention ?? 0
  if (input.theme) {
    setTheme(input.theme)
  }

  const select = lazy(() => input.select ?? Select())
  const allow = lazy(
    () =>
      input.allow ??
      (async (input: any, req: Request) => {
        const redir = new URL(input.redirectURI).hostname
        if (redir === "localhost" || redir === "127.0.0.1") {
          return true
        }
        const forwarded = req.headers.get("x-forwarded-host")
        const host = forwarded
          ? new URL(`https://${forwarded}`).hostname
          : new URL(req.url).hostname

        return isDomainMatch(redir, host)
      }),
  )

  let storage = input.storage
  if (process.env.OPENAUTH_STORAGE) {
    const parsed = JSON.parse(process.env.OPENAUTH_STORAGE)
    if (parsed.type === "dynamo") storage = DynamoStorage(parsed.options)
    if (parsed.type === "memory") storage = MemoryStorage()
    if (parsed.type === "cloudflare")
      throw new Error(
        "Cloudflare storage cannot be configured through env because it requires bindings.",
      )
  }
  if (!storage)
    throw new Error(
      "Store is not configured. Either set the `storage` option or set `OPENAUTH_STORAGE` environment variable.",
    )
  const allSigning = lazy(() =>
    Promise.all([signingKeys(storage), legacySigningKeys(storage)]).then(
      ([a, b]) => [...a, ...b],
    ),
  )
  const allEncryption = lazy(() => encryptionKeys(storage))
  const signingKey = lazy(() => allSigning().then((all) => all[0]))
  const encryptionKey = lazy(() => allEncryption().then((all) => all[0]))

  const auth: Omit<ProviderOptions<any>, "name"> = {
    async success(ctx: Context, properties: any, successOpts) {
      return await input.success(
        {
          async subject(type, properties, subjectOpts) {
            const authorization = await getAuthorization(ctx)
            const subject = subjectOpts?.subject
              ? subjectOpts.subject
              : await resolveSubject(type, properties)
            await successOpts?.invalidate?.(
              await resolveSubject(type, properties),
            )
            if (authorization.response_type === "token") {
              const location = new URL(authorization.redirect_uri)
              const tokens = await generateTokens(ctx, {
                subject,
                type: type as string,
                properties,
                clientID: authorization.client_id,
                ttl: {
                  access: subjectOpts?.ttl?.access ?? ttlAccess,
                  refresh: subjectOpts?.ttl?.refresh ?? ttlRefresh,
                },
              })
              location.hash = new URLSearchParams({
                access_token: tokens.access,
                refresh_token: tokens.refresh,
                state: authorization.state || "",
              }).toString()
              await auth.unset(ctx, "authorization")
              return ctx.redirect(location.toString(), 302)
            }
            if (authorization.response_type === "code") {
              const code = crypto.randomUUID()
              await Storage.set(
                storage,
                ["oauth:code", code],
                {
                  type,
                  properties,
                  subject,
                  redirectURI: authorization.redirect_uri,
                  clientID: authorization.client_id,
                  pkce: authorization.pkce,
                  ttl: {
                    access: subjectOpts?.ttl?.access ?? ttlAccess,
                    refresh: subjectOpts?.ttl?.refresh ?? ttlRefresh,
                  },
                },
                60,
              )
              const location = new URL(authorization.redirect_uri)
              location.searchParams.set("code", code)
              location.searchParams.set("state", authorization.state || "")
              await auth.unset(ctx, "authorization")
              return ctx.redirect(location.toString(), 302)
            }
            throw new OauthError(
              "invalid_request",
              `Unsupported response_type: ${authorization.response_type}`,
            )
          },
        },
        {
          provider: ctx.get("provider"),
          ...properties,
        },
        ctx.req.raw,
      )
    },
    forward(ctx, response) {
      return ctx.newResponse(
        response.body,
        response.status as any,
        Object.fromEntries(response.headers.entries()),
      )
    },
    async set(ctx, key, maxAge, value) {
      setCookie(ctx, key, await encrypt(value), {
        maxAge,
        httpOnly: true,
        ...(ctx.req.url.startsWith("https://")
          ? { secure: true, sameSite: "None" }
          : {}),
      })
    },
    async get(ctx: Context, key: string) {
      const raw = getCookie(ctx, key)
      if (!raw) return
      return decrypt(raw).catch((ex) => {
        console.error("failed to decrypt", key, ex)
      })
    },
    async unset(ctx: Context, key: string) {
      deleteCookie(ctx, key)
    },
    async invalidate(subject: string) {
      // Resolve the scan in case modifications interfere with iteration
      const keys = await Array.fromAsync(
        Storage.scan(this.storage, ["oauth:refresh", subject]),
      )
      for (const [key] of keys) {
        await Storage.remove(this.storage, key)
      }
    },
    storage,
  }

  async function getAuthorization(ctx: Context) {
    const match =
      (await auth.get(ctx, "authorization")) || ctx.get("authorization")
    if (!match) throw new UnknownStateError()
    return match as AuthorizationState
  }

  async function encrypt(value: any) {
    return await new CompactEncrypt(
      new TextEncoder().encode(JSON.stringify(value)),
    )
      .setProtectedHeader({ alg: "RSA-OAEP-512", enc: "A256GCM" })
      .encrypt(await encryptionKey().then((k) => k.public))
  }

  async function resolveSubject(type: string, properties: any) {
    const jsonString = JSON.stringify(properties)
    const encoder = new TextEncoder()
    const data = encoder.encode(jsonString)
    const hashBuffer = await crypto.subtle.digest("SHA-1", data)
    const hashArray = Array.from(new Uint8Array(hashBuffer))
    const hashHex = hashArray
      .map((b) => b.toString(16).padStart(2, "0"))
      .join("")
    return `${type}:${hashHex.slice(0, 16)}`
  }

  async function generateTokens(
    ctx: Context,
    value: {
      type: string
      properties: any
      subject: string
      clientID: string
      ttl: {
        access: number
        refresh: number
      }
      timeUsed?: number
      nextToken?: string
    },
    opts?: {
      generateRefreshToken?: boolean
    },
  ) {
    const refreshToken = value.nextToken ?? crypto.randomUUID()
    if (opts?.generateRefreshToken ?? true) {
      /**
       * Generate and store the next refresh token after the one we are currently returning.
       * Reserving these in advance avoids concurrency issues with multiple refreshes.
       * Similar treatment should be given to any other values that may have race conditions,
       * for example if a jti claim was added to the access token.
       */
      const refreshValue = {
        ...value,
        nextToken: crypto.randomUUID(),
      }
      delete refreshValue.timeUsed
      await Storage.set(
        storage!,
        ["oauth:refresh", value.subject, refreshToken],
        refreshValue,
        value.ttl.refresh,
      )
    }
    const accessTimeUsed = Math.floor((value.timeUsed ?? Date.now()) / 1000)
    return {
      access: await new SignJWT({
        mode: "access",
        type: value.type,
        properties: value.properties,
        aud: value.clientID,
        iss: issuer(ctx),
        sub: value.subject,
      })
        .setExpirationTime(Math.floor(accessTimeUsed + value.ttl.access))
        .setProtectedHeader(
          await signingKey().then((k) => ({
            alg: k.alg,
            kid: k.id,
            typ: "JWT",
          })),
        )
        .sign(await signingKey().then((item) => item.private)),
      expiresIn: Math.floor(
        accessTimeUsed + value.ttl.access - Date.now() / 1000,
      ),
      refresh: [value.subject, refreshToken].join(":"),
    }
  }

  async function decrypt(value: string) {
    return JSON.parse(
      new TextDecoder().decode(
        await compactDecrypt(
          value,
          await encryptionKey().then((v) => v.private),
        ).then((value) => value.plaintext),
      ),
    )
  }

  function issuer(ctx: Context) {
    return new URL(getRelativeUrl(ctx, "/")).origin
  }

  const app = new Hono<{
    Variables: {
      authorization: AuthorizationState
    }
  }>().use(logger())

  for (const [name, value] of Object.entries(input.providers)) {
    const route = new Hono<any>()
    route.use(async (c, next) => {
      c.set("provider", name)
      await next()
    })
    value.init(route, {
      name,
      ...auth,
    })
    app.route(`/${name}`, route)
  }

  app.get(
    "/.well-known/jwks.json",
    cors({
      origin: "*",
      allowHeaders: ["*"],
      allowMethods: ["GET"],
      credentials: false,
    }),
    async (c) => {
      const all = await allSigning()
      return c.json({
        keys: all.map((item) => ({
          ...item.jwk,
          alg: item.alg,
          exp: item.expired
            ? Math.floor(item.expired.getTime() / 1000)
            : undefined,
        })),
      })
    },
  )

  app.get(
    "/.well-known/oauth-authorization-server",
    cors({
      origin: "*",
      allowHeaders: ["*"],
      allowMethods: ["GET"],
      credentials: false,
    }),
    async (c) => {
      const iss = issuer(c)
      return c.json({
        issuer: iss,
        authorization_endpoint: `${iss}/authorize`,
        token_endpoint: `${iss}/token`,
        jwks_uri: `${iss}/.well-known/jwks.json`,
        response_types_supported: ["code", "token"],
      })
    },
  )

  app.post(
    "/token",
    cors({
      origin: "*",
      allowHeaders: ["*"],
      allowMethods: ["POST"],
      credentials: false,
    }),
    async (c) => {
      const form = await c.req.formData()
      const grantType = form.get("grant_type")

      if (grantType === "authorization_code") {
        const code = form.get("code")
        if (!code)
          return c.json(
            {
              error: "invalid_request",
              error_description: "Missing code",
            },
            400,
          )
        const key = ["oauth:code", code.toString()]
        const payload = await Storage.get<{
          type: string
          properties: any
          clientID: string
          redirectURI: string
          subject: string
          ttl: {
            access: number
            refresh: number
          }
          pkce?: AuthorizationState["pkce"]
        }>(storage, key)
        if (!payload) {
          return c.json(
            {
              error: "invalid_grant",
              error_description: "Authorization code has been used or expired",
            },
            400,
          )
        }
        if (payload.redirectURI !== form.get("redirect_uri")) {
          return c.json(
            {
              error: "invalid_redirect_uri",
              error_description: "Redirect URI mismatch",
            },
            400,
          )
        }
        if (payload.clientID !== form.get("client_id")) {
          return c.json(
            {
              error: "unauthorized_client",
              error_description:
                "Client is not authorized to use this authorization code",
            },
            403,
          )
        }

        if (payload.pkce) {
          const codeVerifier = form.get("code_verifier")?.toString()
          if (!codeVerifier)
            return c.json(
              {
                error: "invalid_grant",
                error_description: "Missing code_verifier",
              },
              400,
            )

          if (
            !(await validatePKCE(
              codeVerifier,
              payload.pkce.challenge,
              payload.pkce.method,
            ))
          ) {
            return c.json(
              {
                error: "invalid_grant",
                error_description: "Code verifier does not match",
              },
              400,
            )
          }
        }
        const tokens = await generateTokens(c, payload)
        await Storage.remove(storage, key)
        return c.json({
          access_token: tokens.access,
          expires_in: tokens.expiresIn,
          refresh_token: tokens.refresh,
        })
      }

      if (grantType === "refresh_token") {
        const refreshToken = form.get("refresh_token")
        if (!refreshToken)
          return c.json(
            {
              error: "invalid_request",
              error_description: "Missing refresh_token",
            },
            400,
          )
        const splits = refreshToken.toString().split(":")
        const token = splits.pop()!
        const subject = splits.join(":")
        const key = ["oauth:refresh", subject, token]
        const payload = await Storage.get<{
          type: string
          properties: any
          clientID: string
          subject: string
          ttl: {
            access: number
            refresh: number
          }
          nextToken: string
          timeUsed?: number
        }>(storage, key)
        if (!payload) {
          return c.json(
            {
              error: "invalid_grant",
              error_description: "Refresh token has been used or expired",
            },
            400,
          )
        }
        const generateRefreshToken = !payload.timeUsed
        if (ttlRefreshReuse <= 0) {
          // no reuse interval, remove the refresh token immediately
          await Storage.remove(storage, key)
        } else if (!payload.timeUsed) {
          payload.timeUsed = Date.now()
          await Storage.set(
            storage,
            key,
            payload,
            ttlRefreshReuse + ttlRefreshRetention,
          )
        } else if (Date.now() > payload.timeUsed + ttlRefreshReuse * 1000) {
          // token was reused past the allowed interval
          await auth.invalidate(subject)
          return c.json(
            {
              error: "invalid_grant",
              error_description: "Refresh token has been used or expired",
            },
            400,
          )
        }
        const tokens = await generateTokens(c, payload, {
          generateRefreshToken,
        })
        return c.json({
          access_token: tokens.access,
          refresh_token: tokens.refresh,
          expires_in: tokens.expiresIn,
        })
      }

      if (grantType === "client_credentials") {
        const provider = form.get("provider")
        if (!provider)
          return c.json({ error: "missing `provider` form value" }, 400)
        const match = input.providers[provider.toString()]
        if (!match)
          return c.json({ error: "invalid `provider` query parameter" }, 400)
        if (!match.client)
          return c.json(
            { error: "this provider does not support client_credentials" },
            400,
          )
        const clientID = form.get("client_id")
        const clientSecret = form.get("client_secret")
        if (!clientID)
          return c.json({ error: "missing `client_id` form value" }, 400)
        if (!clientSecret)
          return c.json({ error: "missing `client_secret` form value" }, 400)
        const response = await match.client({
          clientID: clientID.toString(),
          clientSecret: clientSecret.toString(),
          params: Object.fromEntries(form) as Record<string, string>,
        })
        return input.success(
          {
            async subject(type, properties, opts) {
              const tokens = await generateTokens(c, {
                type: type as string,
                subject:
                  opts?.subject || (await resolveSubject(type, properties)),
                properties,
                clientID: clientID.toString(),
                ttl: {
                  access: opts?.ttl?.access ?? ttlAccess,
                  refresh: opts?.ttl?.refresh ?? ttlRefresh,
                },
              })
              return c.json({
                access_token: tokens.access,
                refresh_token: tokens.refresh,
              })
            },
          },
          {
            provider: provider.toString(),
            ...response,
          },
          c.req.raw,
        )
      }

      throw new Error("Invalid grant_type")
    },
  )

  app.get("/authorize", async (c) => {
    const provider = c.req.query("provider")
    const response_type = c.req.query("response_type")
    const redirect_uri = c.req.query("redirect_uri")
    const state = c.req.query("state")
    const client_id = c.req.query("client_id")
    const audience = c.req.query("audience")
    const code_challenge = c.req.query("code_challenge")
    const code_challenge_method = c.req.query("code_challenge_method")
    const authorization: AuthorizationState = {
      response_type,
      redirect_uri,
      state,
      client_id,
      audience,
      pkce:
        code_challenge && code_challenge_method
          ? {
              challenge: code_challenge,
              method: code_challenge_method,
            }
          : undefined,
    } as AuthorizationState
    c.set("authorization", authorization)

    if (!redirect_uri) {
      return c.text("Missing redirect_uri", { status: 400 })
    }

    if (!response_type) {
      throw new MissingParameterError("response_type")
    }

    if (!client_id) {
      throw new MissingParameterError("client_id")
    }

    if (input.start) {
      await input.start(c.req.raw)
    }

    if (
      !(await allow()(
        {
          clientID: client_id,
          redirectURI: redirect_uri,
          audience,
        },
        c.req.raw,
      ))
    )
      throw new UnauthorizedClientError(client_id, redirect_uri)
    await auth.set(c, "authorization", 60 * 60 * 24, authorization)
    if (provider) return c.redirect(`/${provider}/authorize`)
    const providers = Object.keys(input.providers)
    if (providers.length === 1) return c.redirect(`/${providers[0]}/authorize`)
    return auth.forward(
      c,
      await select()(
        Object.fromEntries(
          Object.entries(input.providers).map(([key, value]) => [
            key,
            value.type,
          ]),
        ),
        c.req.raw,
      ),
    )
  })

  app.get("/userinfo", async (c) => {
    const header = c.req.header("Authorization")

    if (!header) {
      return c.json(
        {
          error: "invalid_request",
          error_description: "Missing Authorization header",
        },
        400,
      )
    }

    const [type, token] = header.split(" ")

    if (type !== "Bearer") {
      return c.json(
        {
          error: "invalid_request",
          error_description: "Missing or invalid Authorization header",
        },
        400,
      )
    }

    if (!token) {
      return c.json(
        {
          error: "invalid_request",
          error_description: "Missing token",
        },
        400,
      )
    }

    const result = await jwtVerify<{
      mode: "access"
      type: keyof SubjectSchema
      properties: v1.InferInput<SubjectSchema[keyof SubjectSchema]>
    }>(token, () => signingKey().then((item) => item.public), {
      issuer: issuer(c),
    })

    const validated = await input.subjects[result.payload.type][
      "~standard"
    ].validate(result.payload.properties)

    if (!validated.issues && result.payload.mode === "access") {
      return c.json(validated.value as SubjectSchema)
    }

    return c.json({
      error: "invalid_token",
      error_description: "Invalid token",
    })
  })

  app.onError(async (err, c) => {
    console.error(err)
    if (err instanceof UnknownStateError) {
      return auth.forward(c, await error(err, c.req.raw))
    }
    const authorization = await getAuthorization(c)
    const url = new URL(authorization.redirect_uri)
    const oauth =
      err instanceof OauthError
        ? err
        : new OauthError("server_error", err.message)
    url.searchParams.set("error", oauth.error)
    url.searchParams.set("error_description", oauth.description)
    return c.redirect(url.toString())
  })

  return app
}


================================================
FILE: packages/openauth/src/jwt.ts
================================================
import { JWTPayload, jwtVerify, KeyLike, SignJWT } from "jose"

export namespace jwt {
  export function create(
    payload: JWTPayload,
    algorithm: string,
    privateKey: KeyLike,
  ) {
    return new SignJWT(payload)
      .setProtectedHeader({ alg: algorithm, typ: "JWT", kid: "sst" })
      .sign(privateKey)
  }

  export function verify<T>(token: string, publicKey: KeyLike) {
    return jwtVerify<T>(token, publicKey)
  }
}


================================================
FILE: packages/openauth/src/keys.ts
================================================
import {
  exportJWK,
  exportPKCS8,
  exportSPKI,
  generateKeyPair,
  importPKCS8,
  importSPKI,
  JWK,
  KeyLike,
} from "jose"
import { Storage, StorageAdapter } from "./storage/storage.js"

const signingAlg = "ES256"
const encryptionAlg = "RSA-OAEP-512"

interface SerializedKeyPair {
  id: string
  publicKey: string
  privateKey: string
  created: number
  alg: string
  expired?: number
}

export interface KeyPair {
  id: string
  alg: string
  public: KeyLike
  private: KeyLike
  created: Date
  expired?: Date
  jwk: JWK
}

/**
 * @deprecated use `signingKeys` instead
 */
export async function legacySigningKeys(
  storage: StorageAdapter,
): Promise<KeyPair[]> {
  const alg = "RS512"
  const results = [] as KeyPair[]
  const scanner = Storage.scan<SerializedKeyPair>(storage, ["oauth:key"])
  for await (const [_key, value] of scanner) {
    const publicKey = await importSPKI(value.publicKey, alg, {
      extractable: true,
    })
    const privateKey = await importPKCS8(value.privateKey, alg)
    const jwk = await exportJWK(publicKey)
    jwk.kid = value.id
    results.push({
      id: value.id,
      alg,
      created: new Date(value.created),
      public: publicKey,
      private: privateKey,
      expired: new Date(1735858114000),
      jwk,
    })
  }
  return results
}

export async function signingKeys(storage: StorageAdapter): Promise<KeyPair[]> {
  const results = [] as KeyPair[]
  const scanner = Storage.scan<SerializedKeyPair>(storage, ["signing:key"])
  for await (const [_key, value] of scanner) {
    const publicKey = await importSPKI(value.publicKey, value.alg, {
      extractable: true,
    })
    const privateKey = await importPKCS8(value.privateKey, value.alg)
    const jwk = await exportJWK(publicKey)
    jwk.kid = value.id
    jwk.use = "sig"
    results.push({
      id: value.id,
      alg: signingAlg,
      created: new Date(value.created),
      expired: value.expired ? new Date(value.expired) : undefined,
      public: publicKey,
      private: privateKey,
      jwk,
    })
  }
  results.sort((a, b) => b.created.getTime() - a.created.getTime())
  if (results.filter((item) => !item.expired).length) return results

  const key = await generateKeyPair(signingAlg, {
    extractable: true,
  })
  const serialized: SerializedKeyPair = {
    id: crypto.randomUUID(),
    publicKey: await exportSPKI(key.publicKey),
    privateKey: await exportPKCS8(key.privateKey),
    created: Date.now(),
    alg: signingAlg,
  }
  await Storage.set(storage, ["signing:key", serialized.id], serialized)
  return signingKeys(storage)
}

export async function encryptionKeys(
  storage: StorageAdapter,
): Promise<KeyPair[]> {
  const results = [] as KeyPair[]
  const scanner = Storage.scan<SerializedKeyPair>(storage, ["encryption:key"])
  for await (const [_key, value] of scanner) {
    const publicKey = await importSPKI(value.publicKey, value.alg, {
      extractable: true,
    })
    const privateKey = await importPKCS8(value.privateKey, value.alg)
    const jwk = await exportJWK(publicKey)
    jwk.kid = value.id
    results.push({
      id: value.id,
      alg: encryptionAlg,
      created: new Date(value.created),
      expired: value.expired ? new Date(value.expired) : undefined,
      public: publicKey,
      private: privateKey,
      jwk,
    })
  }
  results.sort((a, b) => b.created.getTime() - a.created.getTime())
  if (results.filter((item) => !item.expired).length) return results

  const key = await generateKeyPair(encryptionAlg, {
    extractable: true,
  })
  const serialized: SerializedKeyPair = {
    id: crypto.randomUUID(),
    publicKey: await exportSPKI(key.publicKey),
    privateKey: await exportPKCS8(key.privateKey),
    created: Date.now(),
    alg: encryptionAlg,
  }
  await Storage.set(storage, ["encryption:key", serialized.id], serialized)
  return encryptionKeys(storage)
}


================================================
FILE: packages/openauth/src/pkce.ts
================================================
import { base64url } from "jose"

function generateVerifier(length: number): string {
  const buffer = new Uint8Array(length)
  crypto.getRandomValues(buffer)
  return base64url.encode(buffer)
}

async function generateChallenge(verifier: string, method: "S256" | "plain") {
  if (method === "plain") return verifier
  const encoder = new TextEncoder()
  const data = encoder.encode(verifier)
  const hash = await crypto.subtle.digest("SHA-256", data)
  return base64url.encode(new Uint8Array(hash))
}

export async function generatePKCE(length: number = 64) {
  if (length < 43 || length > 128) {
    throw new Error(
      "Code verifier length must be between 43 and 128 characters",
    )
  }
  const verifier = generateVerifier(length)
  const challenge = await generateChallenge(verifier, "S256")
  return {
    verifier,
    challenge,
    method: "S256",
  }
}

export async function validatePKCE(
  verifier: string,
  challenge: string,
  method: "S256" | "plain" = "S256",
) {
  const generatedChallenge = await generateChallenge(verifier, method)
  // timing safe equals?
  return generatedChallenge === challenge
}


================================================
FILE: packages/openauth/src/provider/apple.ts
================================================
/**
 * Use this provider to authenticate with Apple. Supports both OAuth2 and OIDC.
 *
 * #### Using OAuth
 *
 * ```ts {5-8}
 * import { AppleProvider } from "@openauthjs/openauth/provider/apple"
 *
 * export default issuer({
 *   providers: {
 *     apple: AppleProvider({
 *       clientID: "1234567890",
 *       clientSecret: "0987654321"
 *     })
 *   }
 * })
 * ```
 *
 * #### Using OAuth with form_post response mode
 *
 * When requesting name or email scopes from Apple, you must use form_post response mode:
 *
 * ```ts {5-9}
 * import { AppleProvider } from "@openauthjs/openauth/provider/apple"
 *
 * export default issuer({
 *   providers: {
 *     apple: AppleProvider({
 *       clientID: "1234567890",
 *       clientSecret: "0987654321",
 *       responseMode: "form_post"
 *     })
 *   }
 * })
 * ```
 *
 * #### Using OIDC
 *
 * ```ts {5-7}
 * import { AppleOidcProvider } from "@openauthjs/openauth/provider/apple"
 *
 * export default issuer({
 *   providers: {
 *     apple: AppleOidcProvider({
 *       clientID: "1234567890"
 *     })
 *   }
 * })
 * ```
 *
 * @packageDocumentation
 */

import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js"
import { OidcProvider, OidcWrappedConfig } from "./oidc.js"

export interface AppleConfig extends Oauth2WrappedConfig {
  /**
   * The response mode to use for the authorization request.
   * Apple requires 'form_post' response mode when requesting name or email scopes.
   * @default "query"
   */
  responseMode?: "query" | "form_post"
}
export interface AppleOidcConfig extends OidcWrappedConfig {}

/**
 * Create an Apple OAuth2 provider.
 *
 * @param config - The config for the provider.
 * @example
 * ```ts
 * // Using default query response mode (GET callback)
 * AppleProvider({
 *   clientID: "1234567890",
 *   clientSecret: "0987654321"
 * })
 *
 * // Using form_post response mode (POST callback)
 * // Required when requesting name or email scope
 * AppleProvider({
 *   clientID: "1234567890",
 *   clientSecret: "0987654321",
 *   responseMode: "form_post",
 *   scopes: ["name", "email"]
 * })
 * ```
 */
export function AppleProvider(config: AppleConfig) {
  const { responseMode, ...restConfig } = config
  const additionalQuery =
    responseMode === "form_post"
      ? { response_mode: "form_post", ...config.query }
      : config.query || {}

  return Oauth2Provider({
    ...restConfig,
    type: "apple" as const,
    endpoint: {
      authorization: "https://appleid.apple.com/auth/authorize",
      token: "https://appleid.apple.com/auth/token",
      jwks: "https://appleid.apple.com/auth/keys",
    },
    query: additionalQuery,
  })
}

/**
 * Create an Apple OIDC provider.
 *
 * This is useful if you just want to verify the user's email address.
 *
 * @param config - The config for the provider.
 * @example
 * ```ts
 * AppleOidcProvider({
 *   clientID: "1234567890"
 * })
 * ```
 */
export function AppleOidcProvider(config: AppleOidcConfig) {
  return OidcProvider({
    ...config,
    type: "apple" as const,
    issuer: "https://appleid.apple.com",
  })
}


================================================
FILE: packages/openauth/src/provider/arctic.ts
================================================
import type { OAuth2Tokens } from "arctic"
import { Context } from "hono"
import { Provider } from "./provider.js"
import { OauthError } from "../error.js"
import { getRelativeUrl } from "../util.js"

export interface ArcticProviderOptions {
  scopes: string[]
  clientID: string
  clientSecret: string
  query?: Record<string, string>
}

interface ProviderState {
  state: string
}

export function ArcticProvider(
  provider: new (
    clientID: string,
    clientSecret: string,
    callback: string,
  ) => {
    createAuthorizationURL(state: string, scopes: string[]): URL
    validateAuthorizationCode(code: string): Promise<OAuth2Tokens>
    refreshAccessToken(refreshToken: string): Promise<OAuth2Tokens>
  },
  config: ArcticProviderOptions,
): Provider<{
  tokenset: OAuth2Tokens
}> {
  function getClient(c: Context) {
    const callback = new URL(c.req.url)
    const pathname = callback.pathname.replace(/authorize.*$/, "callback")
    const url = getRelativeUrl(c, pathname)
    return new provider(config.clientID, config.clientSecret, url)
  }
  return {
    type: "arctic",
    init(routes, ctx) {
      routes.get("/authorize", async (c) => {
        const client = getClient(c)
        const state = crypto.randomUUID()
        await ctx.set(c, "provider", 60 * 10, {
          state,
        })
        return c.redirect(client.createAuthorizationURL(state, config.scopes))
      })

      routes.get("/callback", async (c) => {
        const client = getClient(c)
        const provider = (await ctx.get(c, "provider")) as ProviderState
        if (!provider) return c.redirect("../authorize")
        const code = c.req.query("code")
        const state = c.req.query("state")
        if (!code) throw new Error("Missing code")
        if (state !== provider.state)
          throw new OauthError("invalid_request", "Invalid state")
        const tokens = await client.validateAuthorizationCode(code)
        return ctx.success(c, {
          tokenset: tokens,
        })
      })
    },
  }
}


================================================
FILE: packages/openauth/src/provider/code.ts
================================================
/**
 * Configures a provider that supports pin code authentication. This is usually paired with the
 * `CodeUI`.
 *
 * ```ts
 * import { CodeUI } from "@openauthjs/openauth/ui/code"
 * import { CodeProvider } from "@openauthjs/openauth/provider/code"
 *
 * export default issuer({
 *   providers: {
 *     code: CodeProvider(
 *       CodeUI({
 *         copy: {
 *           code_info: "We'll send a pin code to your email"
 *         },
 *         sendCode: (claims, code) => console.log(claims.email, code)
 *       })
 *     )
 *   },
 *   // ...
 * })
 * ```
 *
 * You can customize the provider using.
 *
 * ```ts {7-9}
 * const ui = CodeUI({
 *   // ...
 * })
 *
 * export default issuer({
 *   providers: {
 *     code: CodeProvider(
 *       { ...ui, length: 4 }
 *     )
 *   },
 *   // ...
 * })
 * ```
 *
 * Behind the scenes, the `CodeProvider` expects callbacks that implements request handlers
 * that generate the UI for the following.
 *
 * ```ts
 * CodeProvider({
 *   // ...
 *   request: (req, state, form, error) => Promise<Response>
 * })
 * ```
 *
 * This allows you to create your own UI.
 *
 * @packageDocumentation
 */
import { Context } from "hono"
import { Provider } from "./provider.js"
import { generateUnbiasedDigits, timingSafeCompare } from "../random.js"

export interface CodeProviderConfig<
  Claims extends Record<string, string> = Record<string, string>,
> {
  /**
   * The length of the pin code.
   *
   * @default 6
   */
  length?: number
  /**
   * The request handler to generate the UI for the code flow.
   *
   * Takes the standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
   * and optionally [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
   * ojects.
   *
   * Also passes in the current `state` of the flow and any `error` that occurred.
   *
   * Expects the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object
   * in return.
   */
  request: (
    req: Request,
    state: CodeProviderState,
    form?: FormData,
    error?: CodeProviderError,
  ) => Promise<Response>
  /**
   * Callback to send the pin code to the user.
   *
   * @example
   * ```ts
   * {
   *   sendCode: async (claims, code) => {
   *     // Send the code through the email or phone number based on the claims
   *   }
   * }
   * ```
   */
  sendCode: (claims: Claims, code: string) => Promise<void | CodeProviderError>
}

/**
 * The state of the code flow.
 *
 * | State | Description |
 * | ----- | ----------- |
 * | `start` | The user is asked to enter their email address or phone number to start the flow. |
 * | `code` | The user needs to enter the pin code to verify their _claim_. |
 */
export type CodeProviderState =
  | {
      type: "start"
    }
  | {
      type: "code"
      resend?: boolean
      code: string
      claims: Record<string, string>
    }

/**
 * The errors that can happen on the code flow.
 *
 * | Error | Description |
 * | ----- | ----------- |
 * | `invalid_code` | The code is invalid. |
 * | `invalid_claim` | The _claim_, email or phone number, is invalid. |
 */
export type CodeProviderError =
  | {
      type: "invalid_code"
    }
  | {
      type: "invalid_claim"
      key: string
      value: string
    }

export function CodeProvider<
  Claims extends Record<string, string> = Record<string, string>,
>(config: CodeProviderConfig<Claims>): Provider<{ claims: Claims }> {
  const length = config.length || 6
  function generate() {
    return generateUnbiasedDigits(length)
  }

  return {
    type: "code",
    init(routes, ctx) {
      async function transition(
        c: Context,
        next: CodeProviderState,
        fd?: FormData,
        err?: CodeProviderError,
      ) {
        await ctx.set<CodeProviderState>(c, "provider", 60 * 60 * 24, next)
        const resp = ctx.forward(
          c,
          await config.request(c.req.raw, next, fd, err),
        )
        return resp
      }
      routes.get("/authorize", async (c) => {
        const resp = await transition(c, {
          type: "start",
        })
        return resp
      })

      routes.post("/authorize", async (c) => {
        const code = generate()
        const fd = await c.req.formData()
        const state = await ctx.get<CodeProviderState>(c, "provider")
        const action = fd.get("action")?.toString()

        if (action === "request" || action === "resend") {
          const claims = Object.fromEntries(fd) as Claims
          delete claims.action
          const err = await config.sendCode(claims, code)
          if (err) return transition(c, { type: "start" }, fd, err)
          return transition(
            c,
            {
              type: "code",
              resend: action === "resend",
              claims,
              code,
            },
            fd,
          )
        }

        if (
          fd.get("action")?.toString() === "verify" &&
          state.type === "code"
        ) {
          const fd = await c.req.formData()
          const compare = fd.get("code")?.toString()
          if (
            !state.code ||
            !compare ||
            !timingSafeCompare(state.code, compare)
          ) {
            return transition(
              c,
              {
                ...state,
                resend: false,
              },
              fd,
              { type: "invalid_code"
Download .txt
gitextract_6_z9n4fm/

├── .changeset/
│   ├── README.md
│   ├── commit.cjs
│   ├── config.json
│   ├── popular-geese-reply.md
│   ├── stupid-boats-play.md
│   └── ten-pans-invent.md
├── .github/
│   ├── CODE_OF_CONDUCT
│   └── workflows/
│       ├── docs.yml
│       ├── format.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .prettierrc
├── CNAME
├── LICENSE
├── README.md
├── bun.lockb
├── bunfig.toml
├── examples/
│   ├── .gitignore
│   ├── README.md
│   ├── client/
│   │   ├── astro/
│   │   │   ├── .gitignore
│   │   │   ├── .vscode/
│   │   │   │   ├── extensions.json
│   │   │   │   └── launch.json
│   │   │   ├── README.md
│   │   │   ├── astro.config.mjs
│   │   │   ├── package.json
│   │   │   ├── src/
│   │   │   │   ├── auth.ts
│   │   │   │   ├── components/
│   │   │   │   │   └── Welcome.astro
│   │   │   │   ├── env.d.ts
│   │   │   │   ├── layouts/
│   │   │   │   │   └── Layout.astro
│   │   │   │   ├── middleware.ts
│   │   │   │   └── pages/
│   │   │   │       ├── callback.ts
│   │   │   │       └── index.astro
│   │   │   └── tsconfig.json
│   │   ├── cloudflare-api/
│   │   │   ├── api.ts
│   │   │   └── package.json
│   │   ├── jwt-api/
│   │   │   ├── CHANGELOG.md
│   │   │   ├── README.md
│   │   │   ├── index.ts
│   │   │   └── package.json
│   │   ├── lambda-api/
│   │   │   ├── api.ts
│   │   │   └── package.json
│   │   ├── nextjs/
│   │   │   ├── .gitignore
│   │   │   ├── CHANGELOG.md
│   │   │   ├── README.md
│   │   │   ├── app/
│   │   │   │   ├── actions.ts
│   │   │   │   ├── api/
│   │   │   │   │   └── callback/
│   │   │   │   │       └── route.ts
│   │   │   │   ├── auth.ts
│   │   │   │   ├── globals.css
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── page.module.css
│   │   │   │   └── page.tsx
│   │   │   ├── next.config.ts
│   │   │   ├── package.json
│   │   │   └── tsconfig.json
│   │   ├── react/
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src/
│   │   │   │   ├── App.tsx
│   │   │   │   ├── AuthContext.tsx
│   │   │   │   ├── main.tsx
│   │   │   │   └── vite-env.d.ts
│   │   │   ├── tsconfig.app.json
│   │   │   ├── tsconfig.json
│   │   │   ├── tsconfig.node.json
│   │   │   └── vite.config.ts
│   │   └── sveltekit/
│   │       ├── .npmrc
│   │       ├── package.json
│   │       ├── src/
│   │       │   ├── app.d.ts
│   │       │   ├── app.html
│   │       │   ├── hooks.server.ts
│   │       │   ├── lib/
│   │       │   │   └── auth.server.ts
│   │       │   └── routes/
│   │       │       ├── +page.server.ts
│   │       │       ├── +page.svelte
│   │       │       └── callback/
│   │       │           └── +server.ts
│   │       ├── svelte.config.js
│   │       ├── tsconfig.json
│   │       └── vite.config.ts
│   ├── issuer/
│   │   ├── bun/
│   │   │   ├── .gitignore
│   │   │   ├── issuer.ts
│   │   │   └── package.json
│   │   ├── cloudflare/
│   │   │   ├── issuer.ts
│   │   │   ├── package.json
│   │   │   ├── sst-env.d.ts
│   │   │   └── sst.config.ts
│   │   ├── custom-frontend/
│   │   │   ├── auth/
│   │   │   │   ├── issuer.ts
│   │   │   │   └── package.json
│   │   │   ├── frontend/
│   │   │   │   ├── frontend.tsx
│   │   │   │   └── package.json
│   │   │   └── package.json
│   │   ├── lambda/
│   │   │   ├── issuer.ts
│   │   │   ├── package.json
│   │   │   ├── sst-env.d.ts
│   │   │   └── sst.config.ts
│   │   └── node/
│   │       ├── .gitignore
│   │       ├── authorizer.ts
│   │       └── package.json
│   ├── quickstart/
│   │   ├── sst/
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   ├── app/
│   │   │   │   ├── actions.ts
│   │   │   │   ├── api/
│   │   │   │   │   └── callback/
│   │   │   │   │       └── route.ts
│   │   │   │   ├── auth.ts
│   │   │   │   ├── globals.css
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── page.module.css
│   │   │   │   └── page.tsx
│   │   │   ├── auth/
│   │   │   │   ├── index.ts
│   │   │   │   └── subjects.ts
│   │   │   ├── next.config.ts
│   │   │   ├── package.json
│   │   │   ├── sst-env.d.ts
│   │   │   ├── sst.config.ts
│   │   │   └── tsconfig.json
│   │   └── standalone/
│   │       ├── .gitignore
│   │       ├── README.md
│   │       ├── app/
│   │       │   ├── actions.ts
│   │       │   ├── api/
│   │       │   │   └── callback/
│   │       │   │       └── route.ts
│   │       │   ├── auth.ts
│   │       │   ├── globals.css
│   │       │   ├── layout.tsx
│   │       │   ├── page.module.css
│   │       │   └── page.tsx
│   │       ├── auth/
│   │       │   ├── index.ts
│   │       │   └── subjects.ts
│   │       ├── bun.lockb
│   │       ├── next.config.ts
│   │       ├── package.json
│   │       └── tsconfig.json
│   ├── subjects.ts
│   └── tsconfig.json
├── package.json
├── packages/
│   └── openauth/
│       ├── CHANGELOG.md
│       ├── bunfig.toml
│       ├── package.json
│       ├── script/
│       │   └── build.ts
│       ├── src/
│       │   ├── client.ts
│       │   ├── css.d.ts
│       │   ├── error.ts
│       │   ├── index.ts
│       │   ├── issuer.ts
│       │   ├── jwt.ts
│       │   ├── keys.ts
│       │   ├── pkce.ts
│       │   ├── provider/
│       │   │   ├── apple.ts
│       │   │   ├── arctic.ts
│       │   │   ├── code.ts
│       │   │   ├── cognito.ts
│       │   │   ├── discord.ts
│       │   │   ├── facebook.ts
│       │   │   ├── github.ts
│       │   │   ├── google.ts
│       │   │   ├── index.ts
│       │   │   ├── jumpcloud.ts
│       │   │   ├── keycloak.ts
│       │   │   ├── linkedin.ts
│       │   │   ├── microsoft.ts
│       │   │   ├── oauth2.ts
│       │   │   ├── oidc.ts
│       │   │   ├── password.ts
│       │   │   ├── provider.ts
│       │   │   ├── slack.ts
│       │   │   ├── spotify.ts
│       │   │   ├── twitch.ts
│       │   │   ├── x.ts
│       │   │   └── yahoo.ts
│       │   ├── random.ts
│       │   ├── storage/
│       │   │   ├── aws.ts
│       │   │   ├── cloudflare.ts
│       │   │   ├── dynamo.ts
│       │   │   ├── memory.ts
│       │   │   └── storage.ts
│       │   ├── subject.ts
│       │   ├── ui/
│       │   │   ├── base.tsx
│       │   │   ├── code.tsx
│       │   │   ├── form.tsx
│       │   │   ├── icon.tsx
│       │   │   ├── password.tsx
│       │   │   ├── select.tsx
│       │   │   ├── theme.ts
│       │   │   └── ui.css
│       │   └── util.ts
│       ├── test/
│       │   ├── client.test.ts
│       │   ├── issuer.test.ts
│       │   ├── scrap.test.ts
│       │   ├── storage.test.ts
│       │   └── util.test.ts
│       └── tsconfig.json
├── scripts/
│   └── format
└── www/
    ├── .gitignore
    ├── .vscode/
    │   ├── extensions.json
    │   └── launch.json
    ├── README.md
    ├── astro.config.mjs
    ├── bun.lockb
    ├── config.ts
    ├── generate.ts
    ├── package.json
    ├── src/
    │   ├── components/
    │   │   ├── Hero.astro
    │   │   └── Lander.astro
    │   ├── content/
    │   │   ├── config.ts
    │   │   └── docs/
    │   │       ├── docs/
    │   │       │   ├── client.mdx
    │   │       │   ├── index.mdx
    │   │       │   ├── issuer.mdx
    │   │       │   ├── provider/
    │   │       │   │   ├── apple.mdx
    │   │       │   │   ├── code.mdx
    │   │       │   │   ├── cognito.mdx
    │   │       │   │   ├── discord.mdx
    │   │       │   │   ├── facebook.mdx
    │   │       │   │   ├── github.mdx
    │   │       │   │   ├── google.mdx
    │   │       │   │   ├── jumpcloud.mdx
    │   │       │   │   ├── keycloak.mdx
    │   │       │   │   ├── microsoft.mdx
    │   │       │   │   ├── oauth2.mdx
    │   │       │   │   ├── oidc.mdx
    │   │       │   │   ├── password.mdx
    │   │       │   │   ├── slack.mdx
    │   │       │   │   ├── spotify.mdx
    │   │       │   │   ├── twitch.mdx
    │   │       │   │   ├── x.mdx
    │   │       │   │   └── yahoo.mdx
    │   │       │   ├── start/
    │   │       │   │   ├── sst.mdx
    │   │       │   │   └── standalone.mdx
    │   │       │   ├── storage/
    │   │       │   │   ├── cloudflare.mdx
    │   │       │   │   ├── dynamo.mdx
    │   │       │   │   └── memory.mdx
    │   │       │   ├── subject.mdx
    │   │       │   └── ui/
    │   │       │       ├── code.mdx
    │   │       │       ├── password.mdx
    │   │       │       ├── select.mdx
    │   │       │       └── theme.mdx
    │   │       └── index.mdx
    │   ├── custom.css
    │   ├── env.d.ts
    │   └── styles/
    │       └── lander.css
    └── tsconfig.json
Download .txt
SYMBOL INDEX (279 symbols across 86 files)

FILE: examples/client/astro/src/auth.ts
  function setTokens (line 10) | function setTokens(ctx: APIContext, access: string, refresh: string) {

FILE: examples/client/astro/src/env.d.ts
  type Locals (line 6) | interface Locals {

FILE: examples/client/cloudflare-api/api.ts
  type Env (line 5) | interface Env {
  method fetch (line 12) | async fetch(request: Request, env: Env) {
  function setSession (line 67) | function setSession(response: Response, access: string, refresh: string) {

FILE: examples/client/jwt-api/index.ts
  method fetch (line 17) | async fetch(req) {

FILE: examples/client/lambda-api/api.ts
  function setSession (line 52) | function setSession(c: Context, accessToken?: string, refreshToken?: str...

FILE: examples/client/nextjs/app/actions.ts
  function auth (line 7) | async function auth() {
  function login (line 30) | async function login() {
  function logout (line 55) | async function logout() {

FILE: examples/client/nextjs/app/api/callback/route.ts
  function GET (line 4) | async function GET(req: NextRequest) {

FILE: examples/client/nextjs/app/auth.ts
  function setTokens (line 10) | async function setTokens(access: string, refresh: string) {

FILE: examples/client/nextjs/app/layout.tsx
  function RootLayout (line 20) | function RootLayout({

FILE: examples/client/nextjs/app/page.tsx
  function Home (line 5) | async function Home() {

FILE: examples/client/react/src/App.tsx
  function App (line 4) | function App() {

FILE: examples/client/react/src/AuthContext.tsx
  type AuthContextType (line 16) | interface AuthContextType {
  function AuthProvider (line 27) | function AuthProvider({ children }: { children: ReactNode }) {
  function useAuth (line 151) | function useAuth() {

FILE: examples/client/sveltekit/src/app.d.ts
  type Locals (line 6) | interface Locals {

FILE: examples/client/sveltekit/src/lib/auth.server.ts
  function createAuthClient (line 4) | function createAuthClient(event: RequestEvent) {
  function setTokens (line 12) | function setTokens(

FILE: examples/client/sveltekit/src/routes/+page.server.ts
  function load (line 1) | async function load(event) {

FILE: examples/client/sveltekit/src/routes/callback/+server.ts
  function GET (line 4) | async function GET(event) {

FILE: examples/issuer/bun/issuer.ts
  function getUser (line 7) | async function getUser(email: string) {
  method allow (line 32) | async allow() {

FILE: examples/issuer/cloudflare/issuer.ts
  type Env (line 11) | interface Env {
  function getUser (line 15) | async function getUser(email: string) {
  method fetch (line 22) | async fetch(request: Request, env: Env, ctx: ExecutionContext) {

FILE: examples/issuer/cloudflare/sst-env.d.ts
  type Resource (line 8) | interface Resource {

FILE: examples/issuer/cloudflare/sst.config.ts
  method app (line 3) | app(input) {
  method run (line 10) | async run() {

FILE: examples/issuer/custom-frontend/auth/issuer.ts
  function getUser (line 6) | async function getUser(email: string) {
  method request (line 22) | async request(req, state, _form, error) {

FILE: examples/issuer/custom-frontend/frontend/frontend.tsx
  function Layout (line 7) | function Layout(props: PropsWithChildren) {

FILE: examples/issuer/lambda/issuer.ts
  function getUser (line 7) | async function getUser(email: string) {

FILE: examples/issuer/lambda/sst-env.d.ts
  type Resource (line 8) | interface Resource {}

FILE: examples/issuer/lambda/sst.config.ts
  method app (line 3) | app(input) {
  method run (line 10) | async run() {

FILE: examples/issuer/node/authorizer.ts
  function getUser (line 8) | async function getUser(email: string) {

FILE: examples/quickstart/sst/app/actions.ts
  function auth (line 8) | async function auth() {
  function login (line 31) | async function login() {
  function logout (line 56) | async function logout() {

FILE: examples/quickstart/sst/app/api/callback/route.ts
  function GET (line 4) | async function GET(req: NextRequest) {

FILE: examples/quickstart/sst/app/auth.ts
  function setTokens (line 10) | async function setTokens(access: string, refresh: string) {

FILE: examples/quickstart/sst/app/layout.tsx
  function RootLayout (line 20) | function RootLayout({

FILE: examples/quickstart/sst/app/page.tsx
  function Home (line 5) | async function Home() {

FILE: examples/quickstart/sst/auth/index.ts
  function getUser (line 8) | async function getUser(email: string) {

FILE: examples/quickstart/sst/sst-env.d.ts
  type Resource (line 8) | interface Resource {

FILE: examples/quickstart/sst/sst.config.ts
  method app (line 4) | app(input) {
  method run (line 12) | async run() {

FILE: examples/quickstart/standalone/app/actions.ts
  function auth (line 8) | async function auth() {
  function login (line 31) | async function login() {
  function logout (line 56) | async function logout() {

FILE: examples/quickstart/standalone/app/api/callback/route.ts
  function GET (line 4) | async function GET(req: NextRequest) {

FILE: examples/quickstart/standalone/app/auth.ts
  function setTokens (line 9) | async function setTokens(access: string, refresh: string) {

FILE: examples/quickstart/standalone/app/layout.tsx
  function RootLayout (line 20) | function RootLayout({

FILE: examples/quickstart/standalone/app/page.tsx
  function Home (line 5) | async function Home() {

FILE: examples/quickstart/standalone/auth/index.ts
  function getUser (line 7) | async function getUser(email: string) {

FILE: packages/openauth/src/client.ts
  type WellKnown (line 62) | interface WellKnown {
  type Tokens (line 80) | interface Tokens {
  type ResponseLike (line 96) | interface ResponseLike {
  type FetchLike (line 100) | type FetchLike = (...args: any[]) => Promise<ResponseLike>
  type Challenge (line 105) | type Challenge = {
  type ClientInput (line 119) | interface ClientInput {
  type AuthorizeOptions (line 153) | interface AuthorizeOptions {
  type AuthorizeResult (line 183) | interface AuthorizeResult {
  type ExchangeSuccess (line 209) | interface ExchangeSuccess {
  type ExchangeError (line 223) | interface ExchangeError {
  type RefreshOptions (line 237) | interface RefreshOptions {
  type RefreshSuccess (line 247) | interface RefreshSuccess {
  type RefreshError (line 263) | interface RefreshError {
  type VerifyOptions (line 277) | interface VerifyOptions {
  type VerifyResult (line 301) | interface VerifyResult<T extends SubjectSchema> {
  type VerifyError (line 329) | interface VerifyError {
  type Client (line 346) | interface Client {
  function createClient (line 547) | function createClient(input: ClientInput): Client {

FILE: packages/openauth/src/error.ts
  class OauthError (line 20) | class OauthError extends Error {
    method constructor (line 21) | constructor(
  class MissingProviderError (line 39) | class MissingProviderError extends OauthError {
    method constructor (line 40) | constructor() {
  class MissingParameterError (line 51) | class MissingParameterError extends OauthError {
    method constructor (line 52) | constructor(public parameter: string) {
  class UnauthorizedClientError (line 60) | class UnauthorizedClientError extends OauthError {
    method constructor (line 61) | constructor(
  class UnknownStateError (line 78) | class UnknownStateError extends Error {
    method constructor (line 79) | constructor() {
  class InvalidSubjectError (line 89) | class InvalidSubjectError extends Error {
    method constructor (line 90) | constructor() {
  class InvalidRefreshTokenError (line 98) | class InvalidRefreshTokenError extends Error {
    method constructor (line 99) | constructor() {
  class InvalidAccessTokenError (line 107) | class InvalidAccessTokenError extends Error {
    method constructor (line 108) | constructor() {
  class InvalidAuthorizationCodeError (line 116) | class InvalidAuthorizationCodeError extends Error {
    method constructor (line 117) | constructor() {

FILE: packages/openauth/src/issuer.ts
  type OnSuccessResponder (line 144) | interface OnSuccessResponder<
  type AuthorizationState (line 169) | interface AuthorizationState {
  type Prettify (line 184) | type Prettify<T> = {
  type IssuerInput (line 209) | interface IssuerInput<
  function issuer (line 444) | function issuer<

FILE: packages/openauth/src/jwt.ts
  function create (line 4) | function create(
  function verify (line 14) | function verify<T>(token: string, publicKey: KeyLike) {

FILE: packages/openauth/src/keys.ts
  type SerializedKeyPair (line 16) | interface SerializedKeyPair {
  type KeyPair (line 25) | interface KeyPair {
  function legacySigningKeys (line 38) | async function legacySigningKeys(
  function signingKeys (line 64) | async function signingKeys(storage: StorageAdapter): Promise<KeyPair[]> {
  function encryptionKeys (line 102) | async function encryptionKeys(

FILE: packages/openauth/src/pkce.ts
  function generateVerifier (line 3) | function generateVerifier(length: number): string {
  function generateChallenge (line 9) | async function generateChallenge(verifier: string, method: "S256" | "pla...
  function generatePKCE (line 17) | async function generatePKCE(length: number = 64) {
  function validatePKCE (line 32) | async function validatePKCE(

FILE: packages/openauth/src/provider/apple.ts
  type AppleConfig (line 57) | interface AppleConfig extends Oauth2WrappedConfig {
  type AppleOidcConfig (line 65) | interface AppleOidcConfig extends OidcWrappedConfig {}
  function AppleProvider (line 89) | function AppleProvider(config: AppleConfig) {
  function AppleOidcProvider (line 121) | function AppleOidcProvider(config: AppleOidcConfig) {

FILE: packages/openauth/src/provider/arctic.ts
  type ArcticProviderOptions (line 7) | interface ArcticProviderOptions {
  type ProviderState (line 14) | interface ProviderState {
  function ArcticProvider (line 18) | function ArcticProvider(

FILE: packages/openauth/src/provider/code.ts
  type CodeProviderConfig (line 59) | interface CodeProviderConfig<
  type CodeProviderState (line 109) | type CodeProviderState =
  type CodeProviderError (line 128) | type CodeProviderError =
  function CodeProvider (line 138) | function CodeProvider<
  type CodeProviderOptions (line 227) | type CodeProviderOptions = Parameters<typeof CodeProvider>[0]

FILE: packages/openauth/src/provider/cognito.ts
  type CognitoConfig (line 24) | interface CognitoConfig extends Oauth2WrappedConfig {
  function CognitoProvider (line 63) | function CognitoProvider(config: CognitoConfig) {

FILE: packages/openauth/src/provider/discord.ts
  type DiscordConfig (line 22) | interface DiscordConfig extends Oauth2WrappedConfig {}
  function DiscordProvider (line 36) | function DiscordProvider(config: DiscordConfig) {

FILE: packages/openauth/src/provider/facebook.ts
  type FacebookConfig (line 39) | interface FacebookConfig extends Oauth2WrappedConfig {}
  type FacebookOidcConfig (line 40) | interface FacebookOidcConfig extends OidcWrappedConfig {}
  function FacebookProvider (line 54) | function FacebookProvider(config: FacebookConfig) {
  function FacebookOidcProvider (line 78) | function FacebookOidcProvider(config: FacebookOidcConfig) {

FILE: packages/openauth/src/provider/github.ts
  type GithubConfig (line 22) | interface GithubConfig extends Oauth2WrappedConfig {}
  function GithubProvider (line 36) | function GithubProvider(config: GithubConfig) {

FILE: packages/openauth/src/provider/google.ts
  type GoogleConfig (line 39) | interface GoogleConfig extends Oauth2WrappedConfig {}
  type GoogleOidcConfig (line 40) | interface GoogleOidcConfig extends OidcWrappedConfig {}
  function GoogleProvider (line 54) | function GoogleProvider(config: GoogleConfig) {
  function GoogleOidcProvider (line 79) | function GoogleOidcProvider(config: GoogleOidcConfig) {

FILE: packages/openauth/src/provider/jumpcloud.ts
  type JumpCloudConfig (line 22) | interface JumpCloudConfig extends Oauth2WrappedConfig {}
  function JumpCloudProvider (line 36) | function JumpCloudProvider(config: JumpCloudConfig) {

FILE: packages/openauth/src/provider/keycloak.ts
  type KeycloakConfig (line 24) | interface KeycloakConfig extends Oauth2WrappedConfig {
  function KeycloakProvider (line 66) | function KeycloakProvider(config: KeycloakConfig) {

FILE: packages/openauth/src/provider/linkedin.ts
  function LinkedInAdapter (line 3) | function LinkedInAdapter(config: Oauth2WrappedConfig) {

FILE: packages/openauth/src/provider/microsoft.ts
  type MicrosoftConfig (line 40) | interface MicrosoftConfig extends Oauth2WrappedConfig {
  type MicrosoftOidcConfig (line 55) | interface MicrosoftOidcConfig extends OidcWrappedConfig {}
  function MicrosoftProvider (line 70) | function MicrosoftProvider(config: MicrosoftConfig) {
  function MicrosoftOidcProvider (line 94) | function MicrosoftOidcProvider(config: MicrosoftOidcConfig) {

FILE: packages/openauth/src/provider/oauth2.ts
  type Oauth2Config (line 31) | interface Oauth2Config {
  type Oauth2WrappedConfig (line 125) | type Oauth2WrappedConfig = Omit<Oauth2Config, "endpoint" | "name">
  type Oauth2Token (line 130) | interface Oauth2Token {
  type ProviderState (line 138) | interface ProviderState {
  function Oauth2Provider (line 144) | function Oauth2Provider(

FILE: packages/openauth/src/provider/oidc.ts
  type OidcConfig (line 28) | interface OidcConfig {
  type OidcWrappedConfig (line 85) | type OidcWrappedConfig = Omit<OidcConfig, "issuer" | "name">
  type ProviderState (line 87) | interface ProviderState {
  type IdTokenResponse (line 96) | interface IdTokenResponse {
  function OidcProvider (line 102) | function OidcProvider(

FILE: packages/openauth/src/provider/password.ts
  type PasswordHasher (line 49) | interface PasswordHasher<T> {
  type PasswordConfig (line 54) | interface PasswordConfig {
  type PasswordRegisterState (line 154) | type PasswordRegisterState =
  type PasswordRegisterError (line 176) | type PasswordRegisterError =
  type PasswordChangeState (line 206) | type PasswordChangeState =
  type PasswordChangeError (line 233) | type PasswordChangeError =
  type PasswordLoginError (line 259) | type PasswordLoginError =
  function PasswordProvider (line 267) | function PasswordProvider(
  type HashedPassword (line 543) | interface HashedPassword {}
  function PBKDF2Hasher (line 548) | function PBKDF2Hasher(opts?: { iterations?: number }): PasswordHasher<{
  function ScryptHasher (line 613) | function ScryptHasher(opts?: {

FILE: packages/openauth/src/provider/provider.ts
  type ProviderRoute (line 4) | type ProviderRoute = Hono
  type Provider (line 6) | interface Provider<Properties = any> {
  type ProviderOptions (line 16) | interface ProviderOptions<Properties> {
  class ProviderError (line 32) | class ProviderError extends Error {}
  class ProviderUnknownError (line 33) | class ProviderUnknownError extends ProviderError {}

FILE: packages/openauth/src/provider/slack.ts
  type SlackConfig (line 24) | interface SlackConfig extends Oauth2WrappedConfig {
  function SlackProvider (line 58) | function SlackProvider(config: SlackConfig) {

FILE: packages/openauth/src/provider/spotify.ts
  type SpotifyConfig (line 22) | interface SpotifyConfig extends Oauth2WrappedConfig {}
  function SpotifyProvider (line 36) | function SpotifyProvider(config: SpotifyConfig) {

FILE: packages/openauth/src/provider/twitch.ts
  type TwitchConfig (line 22) | interface TwitchConfig extends Oauth2WrappedConfig {}
  function TwitchProvider (line 36) | function TwitchProvider(config: TwitchConfig) {

FILE: packages/openauth/src/provider/x.ts
  type XProviderConfig (line 22) | interface XProviderConfig extends Oauth2WrappedConfig {}
  function XProvider (line 36) | function XProvider(config: XProviderConfig) {

FILE: packages/openauth/src/provider/yahoo.ts
  type YahooConfig (line 22) | interface YahooConfig extends Oauth2WrappedConfig {}
  function YahooProvider (line 36) | function YahooProvider(config: YahooConfig) {

FILE: packages/openauth/src/random.ts
  function generateUnbiasedDigits (line 3) | function generateUnbiasedDigits(length: number): string {
  function timingSafeCompare (line 16) | function timingSafeCompare(a: string, b: string): boolean {

FILE: packages/openauth/src/storage/aws.ts
  type EC2Credentials (line 3) | interface EC2Credentials {
  function getCredentials (line 13) | async function getCredentials(url: string): Promise<EC2Credentials> {
  function client (line 30) | async function client(): Promise<AwsClient> {
  type AwsOptions (line 56) | type AwsOptions = Exclude<

FILE: packages/openauth/src/storage/cloudflare.ts
  type CloudflareStorageOptions (line 27) | interface CloudflareStorageOptions {
  function CloudflareStorage (line 34) | function CloudflareStorage(

FILE: packages/openauth/src/storage/dynamo.ts
  type DynamoStorageOptions (line 37) | interface DynamoStorageOptions {
  function DynamoStorage (line 68) | function DynamoStorage(options: DynamoStorageOptions): StorageAdapter {

FILE: packages/openauth/src/storage/memory.ts
  type MemoryStorageOptions (line 38) | interface MemoryStorageOptions {
  function MemoryStorage (line 51) | function MemoryStorage(input?: MemoryStorageOptions): StorageAdapter {

FILE: packages/openauth/src/storage/storage.ts
  type StorageAdapter (line 1) | interface StorageAdapter {
  constant SEPERATOR (line 8) | const SEPERATOR = String.fromCharCode(0x1f)
  function joinKey (line 10) | function joinKey(key: string[]) {
  function splitKey (line 14) | function splitKey(key: string) {
  function encode (line 19) | function encode(key: string[]) {
  function get (line 22) | function get<T>(adapter: StorageAdapter, key: string[]) {
  function set (line 26) | function set(
  function remove (line 36) | function remove(adapter: StorageAdapter, key: string[]) {
  function scan (line 40) | function scan<T>(

FILE: packages/openauth/src/subject.ts
  type SubjectSchema (line 95) | type SubjectSchema = Record<string, v1.StandardSchema>
  type SubjectPayload (line 98) | type SubjectPayload<T extends SubjectSchema> = Prettify<
  function createSubjects (line 126) | function createSubjects<Schema extends SubjectSchema = {}>(

FILE: packages/openauth/src/ui/base.tsx
  function Layout (line 5) | function Layout(
  constant ICON_OPENAUTH (line 104) | const ICON_OPENAUTH = (

FILE: packages/openauth/src/ui/code.tsx
  constant DEFAULT_COPY (line 32) | const DEFAULT_COPY = {
  type CodeUICopy (line 75) | type CodeUICopy = typeof DEFAULT_COPY
  type CodeUIOptions (line 80) | interface CodeUIOptions {
  function CodeUI (line 110) | function CodeUI(props: CodeUIOptions): CodeProviderOptions {

FILE: packages/openauth/src/ui/form.tsx
  function FormAlert (line 3) | function FormAlert(props: {

FILE: packages/openauth/src/ui/icon.tsx
  constant ICON_GITHUB (line 3) | const ICON_GITHUB = (
  constant ICON_GOOGLE (line 16) | const ICON_GOOGLE = (
  constant ICON_EMAIL (line 43) | const ICON_EMAIL = (
  constant ICON_SLACK (line 60) | const ICON_SLACK = (

FILE: packages/openauth/src/ui/password.tsx
  constant DEFAULT_COPY (line 37) | const DEFAULT_COPY = {
  type PasswordUICopy (line 138) | type PasswordUICopy = typeof DEFAULT_COPY
  type PasswordUIOptions (line 143) | interface PasswordUIOptions
  function PasswordUI (line 155) | function PasswordUI(input: PasswordUIOptions): PasswordConfig {

FILE: packages/openauth/src/ui/select.tsx
  type SelectProps (line 30) | interface SelectProps {
  function Select (line 62) | function Select(props?: SelectProps) {
  constant DISPLAY (line 97) | const DISPLAY: Record<string, string> = {
  constant ICON (line 108) | const ICON: Record<string, any> = {

FILE: packages/openauth/src/ui/theme.ts
  type ColorScheme (line 47) | interface ColorScheme {
  type Theme (line 61) | interface Theme {
  constant THEME_OPENAUTH (line 185) | const THEME_OPENAUTH: Theme = {
  constant THEME_TERMINAL (line 207) | const THEME_TERMINAL: Theme = {
  constant THEME_SST (line 231) | const THEME_SST: Theme = {
  constant THEME_SUPABASE (line 254) | const THEME_SUPABASE: Theme = {
  constant THEME_VERCEL (line 280) | const THEME_VERCEL: Theme = {
  function setTheme (line 308) | function setTheme(value: Theme) {
  function getTheme (line 316) | function getTheme() {

FILE: packages/openauth/src/util.ts
  type Prettify (line 3) | type Prettify<T> = {
  function getRelativeUrl (line 7) | function getRelativeUrl(ctx: Context, path: string) {
  function isDomainMatch (line 36) | function isDomainMatch(a: string, b: string): boolean {
  function lazy (line 50) | function lazy<T>(fn: () => T): () => T {

FILE: packages/openauth/test/client.test.ts
  method init (line 44) | init(route, ctx) {

FILE: packages/openauth/test/issuer.test.ts
  method init (line 36) | init(route, ctx) {

FILE: packages/openauth/test/scrap.test.ts
  method init (line 29) | init(route, ctx) {

FILE: www/generate.ts
  constant OUTPUT_DIR (line 7) | const OUTPUT_DIR = "src/content/docs/docs"
  type Text (line 8) | type Text = string | Text[]
  constant FRONTMATTER (line 21) | const FRONTMATTER: Record<
  function renderProvider (line 175) | function renderProvider(module: TypeDoc.DeclarationReflection) {
  function renderStorage (line 195) | function renderStorage(module: TypeDoc.DeclarationReflection) {
  function renderUI (line 215) | function renderUI(module: TypeDoc.DeclarationReflection) {
  function renderSubject (line 236) | function renderSubject() {
  function renderClient (line 257) | function renderClient() {
  function renderIssuer (line 284) | function renderIssuer() {
  function renderHeader (line 308) | function renderHeader(input: {
  function renderAbout (line 327) | function renderAbout(content: Text) {
  function renderClass (line 332) | function renderClass(c: TypeDoc.DeclarationReflection) {
  function renderVariables (line 337) | function renderVariables(
  function renderFunctions (line 354) | function renderFunctions(module: TypeDoc.DeclarationReflection) {
  function renderInterfaces (line 388) | function renderInterfaces(
  function renderComment (line 488) | function renderComment(declaration: TypeDoc.Reflection) {
  function renderSignatureAsCode (line 532) | function renderSignatureAsCode(signature: TypeDoc.SignatureReflection) {
  function renderSignatureAsType (line 537) | function renderSignatureAsType(signature: TypeDoc.SignatureReflection) {
  function renderParameter (line 555) | function renderParameter(parameter: TypeDoc.ParameterReflection) {
  function renderProperty (line 577) | function renderProperty(property: TypeDoc.DeclarationReflection) {
  function renderType (line 581) | function renderType(type: TypeDoc.SomeType): Text {
  function renderIntrisicType (line 622) | function renderIntrisicType(type: TypeDoc.IntrinsicType) {
  function renderLiteralType (line 625) | function renderLiteralType(type: TypeDoc.LiteralType) {
  function renderTemplateLiteralType (line 647) | function renderTemplateLiteralType(type: TypeDoc.TemplateLiteralType) {
  function renderUnionType (line 674) | function renderUnionType(type: TypeDoc.UnionType) {
  function renderArrayType (line 689) | function renderArrayType(type: TypeDoc.ArrayType) {
  function renderCallbackType (line 696) | function renderCallbackType(type: TypeDoc.ReflectionType) {
  function renderObjectTypeInline (line 699) | function renderObjectTypeInline(type: TypeDoc.ReflectionType): Text {
  function renderTypescriptType (line 718) | function renderTypescriptType(type: TypeDoc.ReferenceType) {
  function renderOpenAuthType (line 730) | function renderOpenAuthType(type: TypeDoc.ReferenceType) {
  function renderStandardSchemaType (line 763) | function renderStandardSchemaType(type: TypeDoc.ReferenceType) {
  function render (line 767) | function render(condition: any, content: Text) {
  function buildLinkHash (line 771) | function buildLinkHash(namespace: string, name: string) {
  function flattenNestedTypes (line 775) | function flattenNestedTypes(
  function saveFile (line 827) | function saveFile(moduleName: string, content: any[]) {
  function configureLogger (line 834) | function configureLogger() {
  function printWarnings (line 847) | function printWarnings() {
  function build (line 853) | async function build() {
  function print (line 907) | function print(type: TypeDoc.SomeType) {
Condensed preview — 237 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (641K chars).
[
  {
    "path": ".changeset/README.md",
    "chars": 510,
    "preview": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that wo"
  },
  {
    "path": ".changeset/commit.cjs",
    "chars": 161,
    "preview": "/** @type {import('@changesets/types').CommitFunctions[\"getAddMessage\"]} */\nmodule.exports.getAddMessage = async (change"
  },
  {
    "path": ".changeset/config.json",
    "chars": 329,
    "preview": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@3.0.4/schema.json\",\n  \"changelog\": \"@changesets/cli/changelog\",\n  \""
  },
  {
    "path": ".changeset/popular-geese-reply.md",
    "chars": 93,
    "preview": "---\n\"@openauthjs/openauth\": patch\n---\n\nupdate google icon to comply with branding guidelines\n"
  },
  {
    "path": ".changeset/stupid-boats-play.md",
    "chars": 70,
    "preview": "---\n\"@openauthjs/openauth\": patch\n---\n\nallow auth style autodetection\n"
  },
  {
    "path": ".changeset/ten-pans-invent.md",
    "chars": 60,
    "preview": "---\n\"@openauthjs/openauth\": patch\n---\n\nadd linkedin adapter\n"
  },
  {
    "path": ".github/CODE_OF_CONDUCT",
    "chars": 1434,
    "preview": "# Code of Conduct\n\nI don't typically set up a code of conduct for our projects but given this one is security related it"
  },
  {
    "path": ".github/workflows/docs.yml",
    "chars": 961,
    "preview": "name: docs\n\non:\n  # Trigger the workflow every time you push to the `main` branch\n  # Using a different branch name? Rep"
  },
  {
    "path": ".github/workflows/format.yml",
    "chars": 545,
    "preview": "name: format\n\non:\n  push:\n    branches: [master]\n  pull_request:\n  workflow_dispatch:\n\njobs:\n  format:\n    runs-on: ubun"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 552,
    "preview": "name: release\n\non:\n  push:\n    branches:\n      - master\n\npermissions:\n  contents: write\n  pull-requests: write\n\nconcurre"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 373,
    "preview": "name: test\n\non:\n  push:\n    branches: [master]\n  pull_request:\n  workflow_dispatch:\n\njobs:\n  test:\n    runs-on: ubuntu-l"
  },
  {
    "path": ".gitignore",
    "chars": 79,
    "preview": "/node_modules\n.sst\n.env\ndist\npersist.json\n.DS_Store\nnotes\n.nvim.lua\n.svelte-kit"
  },
  {
    "path": ".prettierrc",
    "chars": 21,
    "preview": "{\n  \"semi\": false,\n}\n"
  },
  {
    "path": "CNAME",
    "chars": 16,
    "preview": "openauth.js.org\n"
  },
  {
    "path": "LICENSE",
    "chars": 1061,
    "preview": "MIT License\n\nCopyright (c) 2024 SST\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof thi"
  },
  {
    "path": "README.md",
    "chars": 13531,
    "preview": "<p align=\"center\">\n  <a href=\"https://openauth.js.org\">\n    <picture>\n      <source srcset=\"https://raw.githubuserconten"
  },
  {
    "path": "bunfig.toml",
    "chars": 23,
    "preview": "[install]\nexact = true\n"
  },
  {
    "path": "examples/.gitignore",
    "chars": 2245,
    "preview": "# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore\n\n# Logs\n\nlogs\n_.log\nnpm-debug.log_\nyar"
  },
  {
    "path": "examples/README.md",
    "chars": 824,
    "preview": "# Examples\n\nThere are two sets of examples here, issuers and clients. Issuers are examples of setting up an OpenAuth ser"
  },
  {
    "path": "examples/client/astro/.gitignore",
    "chars": 264,
    "preview": "# build output\ndist/\n\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyar"
  },
  {
    "path": "examples/client/astro/.vscode/extensions.json",
    "chars": 87,
    "preview": "{\n  \"recommendations\": [\"astro-build.astro-vscode\"],\n  \"unwantedRecommendations\": []\n}\n"
  },
  {
    "path": "examples/client/astro/.vscode/launch.json",
    "chars": 207,
    "preview": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"command\": \"./node_modules/.bin/astro dev\",\n      \"name\": \"Dev"
  },
  {
    "path": "examples/client/astro/README.md",
    "chars": 420,
    "preview": "# OpenAuth Astro Client\n\nThe files to note are\n\n- `src/auth.ts` - creates the client that is used to interact with the a"
  },
  {
    "path": "examples/client/astro/astro.config.mjs",
    "chars": 181,
    "preview": "// @ts-check\nimport { defineConfig } from \"astro/config\";\n\n// https://astro.build/config\nexport default defineConfig({\n "
  },
  {
    "path": "examples/client/astro/package.json",
    "chars": 305,
    "preview": "{\n  \"name\": \"@openauthjs/example-client-astro\",\n  \"type\": \"module\",\n  \"version\": \"0.0.0\",\n  \"scripts\": {\n    \"dev\": \"ast"
  },
  {
    "path": "examples/client/astro/src/auth.ts",
    "chars": 578,
    "preview": "import { createClient } from \"@openauthjs/openauth/client\"\nimport type { APIContext } from \"astro\"\nexport { subjects } f"
  },
  {
    "path": "examples/client/astro/src/components/Welcome.astro",
    "chars": 4918,
    "preview": "---\nimport astroLogo from '../assets/astro.svg';\nimport background from '../assets/background.svg';\n---\n\n<div id=\"contai"
  },
  {
    "path": "examples/client/astro/src/env.d.ts",
    "chars": 228,
    "preview": "import type { SubjectPayload } from \"@openauthjs/openauth/subject\"\nimport { subjects } from \"./auth\"\n\ndeclare global {\n "
  },
  {
    "path": "examples/client/astro/src/layouts/Layout.astro",
    "chars": 396,
    "preview": "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"UTF-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width\""
  },
  {
    "path": "examples/client/astro/src/middleware.ts",
    "chars": 918,
    "preview": "import { defineMiddleware } from \"astro:middleware\"\nimport { subjects } from \"../../../subjects\"\nimport { client, setTok"
  },
  {
    "path": "examples/client/astro/src/pages/callback.ts",
    "chars": 506,
    "preview": "import type { APIRoute } from \"astro\"\nimport { client, setTokens } from \"../auth\"\n\nexport const GET: APIRoute = async (c"
  },
  {
    "path": "examples/client/astro/src/pages/index.astro",
    "chars": 420,
    "preview": "---\nimport Welcome from '../components/Welcome.astro';\nimport Layout from '../layouts/Layout.astro';\n\n// Welcome to Astr"
  },
  {
    "path": "examples/client/astro/tsconfig.json",
    "chars": 109,
    "preview": "{\n  \"extends\": \"astro/tsconfigs/strict\",\n  \"include\": [\".astro/types.d.ts\", \"**/*\"],\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "examples/client/cloudflare-api/api.ts",
    "chars": 2526,
    "preview": "import type { Service } from \"@cloudflare/workers-types\"\nimport { createClient } from \"@openauthjs/openauth/client\"\nimpo"
  },
  {
    "path": "examples/client/cloudflare-api/package.json",
    "chars": 72,
    "preview": "{\n  \"name\": \"cloudflare-api\",\n  \"version\": \"0.0.0\",\n  \"private\": true\n}\n"
  },
  {
    "path": "examples/client/jwt-api/CHANGELOG.md",
    "chars": 104,
    "preview": "# jwt-api\n\n## 1.0.1\n\n### Patch Changes\n\n- Updated dependencies [8b5f490]\n  - @openauthjs/openauth@0.2.4\n"
  },
  {
    "path": "examples/client/jwt-api/README.md",
    "chars": 324,
    "preview": "# JWT API\n\nThis simple API verifies the `Authorization` header using the OpenAuth client and returns the subject.\n\nRun i"
  },
  {
    "path": "examples/client/jwt-api/index.ts",
    "chars": 1104,
    "preview": "import { createClient } from \"@openauthjs/openauth/client\"\nimport { subjects } from \"../../subjects\"\n\nconst headers = {\n"
  },
  {
    "path": "examples/client/jwt-api/package.json",
    "chars": 308,
    "preview": "{\n  \"name\": \"@openauthjs/example-jwt-api\",\n  \"version\": \"1.0.1\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\":"
  },
  {
    "path": "examples/client/lambda-api/api.ts",
    "chars": 2030,
    "preview": "import { Context, Hono } from \"hono\"\nimport { getCookie, setCookie } from \"hono/cookie\"\nimport { createClient } from \"@o"
  },
  {
    "path": "examples/client/lambda-api/package.json",
    "chars": 68,
    "preview": "{\n  \"name\": \"lambda-api\",\n  \"version\": \"0.0.0\",\n  \"private\": true\n}\n"
  },
  {
    "path": "examples/client/nextjs/.gitignore",
    "chars": 480,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "examples/client/nextjs/CHANGELOG.md",
    "chars": 672,
    "preview": "# nextjs\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [8b5f490]\n  - @openauthjs/openauth@0.2.4\n\n## 0.1.5\n\n### Pa"
  },
  {
    "path": "examples/client/nextjs/README.md",
    "chars": 846,
    "preview": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-re"
  },
  {
    "path": "examples/client/nextjs/app/actions.ts",
    "chars": 1550,
    "preview": "\"use server\"\n\nimport { redirect } from \"next/navigation\"\nimport { headers as getHeaders, cookies as getCookies } from \"n"
  },
  {
    "path": "examples/client/nextjs/app/api/callback/route.ts",
    "chars": 507,
    "preview": "import { client, setTokens } from \"../../auth\"\nimport { type NextRequest, NextResponse } from \"next/server\"\n\nexport asyn"
  },
  {
    "path": "examples/client/nextjs/app/auth.ts",
    "chars": 653,
    "preview": "import { createClient } from \"@openauthjs/openauth/client\"\nimport { cookies as getCookies } from \"next/headers\"\nexport {"
  },
  {
    "path": "examples/client/nextjs/app/globals.css",
    "chars": 608,
    "preview": ":root {\n  --background: #ffffff;\n  --foreground: #171717;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --backg"
  },
  {
    "path": "examples/client/nextjs/app/layout.tsx",
    "chars": 654,
    "preview": "import type { Metadata } from \"next\"\nimport { Geist, Geist_Mono } from \"next/font/google\"\nimport \"./globals.css\"\n\nconst "
  },
  {
    "path": "examples/client/nextjs/app/page.module.css",
    "chars": 2789,
    "preview": ".page {\n  --gray-rgb: 0, 0, 0;\n  --gray-alpha-200: rgba(var(--gray-rgb), 0.08);\n  --gray-alpha-100: rgba(var(--gray-rgb)"
  },
  {
    "path": "examples/client/nextjs/app/page.tsx",
    "chars": 2626,
    "preview": "import Image from \"next/image\"\nimport { auth, login, logout } from \"./actions\"\nimport styles from \"./page.module.css\"\n\ne"
  },
  {
    "path": "examples/client/nextjs/next.config.ts",
    "chars": 130,
    "preview": "import type { NextConfig } from \"next\"\n\nconst nextConfig: NextConfig = {\n  /* config options here */\n}\n\nexport default n"
  },
  {
    "path": "examples/client/nextjs/package.json",
    "chars": 492,
    "preview": "{\n  \"name\": \"@openauthjs/example-nextjs\",\n  \"version\": \"0.1.6\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\","
  },
  {
    "path": "examples/client/nextjs/tsconfig.json",
    "chars": 598,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    "
  },
  {
    "path": "examples/client/react/.gitignore",
    "chars": 253,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": "examples/client/react/README.md",
    "chars": 622,
    "preview": "# React SPA Auth\n\nThis uses the token + pkce flow to authenticate a user. Start it using.\n\n```bash\nbun run dev\n```\n\nThen"
  },
  {
    "path": "examples/client/react/index.html",
    "chars": 366,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
  },
  {
    "path": "examples/client/react/package.json",
    "chars": 734,
    "preview": "{\n  \"name\": \"@openauthjs/example-react\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n   "
  },
  {
    "path": "examples/client/react/src/App.tsx",
    "chars": 920,
    "preview": "import { useState } from \"react\"\nimport { useAuth } from \"./AuthContext\"\n\nfunction App() {\n  const auth = useAuth()\n  co"
  },
  {
    "path": "examples/client/react/src/AuthContext.tsx",
    "chars": 3275,
    "preview": "import {\n  useRef,\n  useState,\n  ReactNode,\n  useEffect,\n  useContext,\n  createContext,\n} from \"react\"\nimport { createCl"
  },
  {
    "path": "examples/client/react/src/main.tsx",
    "chars": 291,
    "preview": "import { StrictMode } from \"react\"\nimport { createRoot } from \"react-dom/client\"\nimport { AuthProvider } from \"./AuthCon"
  },
  {
    "path": "examples/client/react/src/vite-env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/client/react/tsconfig.app.json",
    "chars": 665,
    "preview": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2020\",\n"
  },
  {
    "path": "examples/client/react/tsconfig.json",
    "chars": 139,
    "preview": "{\n  \"files\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.app.json\"\n    },\n    {\n      \"path\": \"./tsconfig.node"
  },
  {
    "path": "examples/client/react/tsconfig.node.json",
    "chars": 593,
    "preview": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2022\","
  },
  {
    "path": "examples/client/react/vite.config.ts",
    "chars": 161,
    "preview": "import { defineConfig } from \"vite\"\nimport react from \"@vitejs/plugin-react\"\n\n// https://vite.dev/config/\nexport default"
  },
  {
    "path": "examples/client/sveltekit/.npmrc",
    "chars": 19,
    "preview": "engine-strict=true\n"
  },
  {
    "path": "examples/client/sveltekit/package.json",
    "chars": 722,
    "preview": "{\n  \"name\": \"@openauthjs/example-client-sveltekit\",\n  \"type\": \"module\",\n  \"version\": \"0.0.0\",\n  \"scripts\": {\n    \"dev\": "
  },
  {
    "path": "examples/client/sveltekit/src/app.d.ts",
    "chars": 317,
    "preview": "// See https://svelte.dev/docs/kit/types#app.d.ts\n// for information about these interfaces\ndeclare global {\n  namespace"
  },
  {
    "path": "examples/client/sveltekit/src/app.html",
    "chars": 346,
    "preview": "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<link rel=\"icon\" href=\"%sveltekit.assets%/favicon."
  },
  {
    "path": "examples/client/sveltekit/src/hooks.server.ts",
    "chars": 964,
    "preview": "import { redirect, type Handle } from \"@sveltejs/kit\"\nimport { createAuthClient, setTokens } from \"$lib/auth.server\"\nimp"
  },
  {
    "path": "examples/client/sveltekit/src/lib/auth.server.ts",
    "chars": 656,
    "preview": "import { createClient } from \"@openauthjs/openauth/client\"\nimport type { RequestEvent } from \"@sveltejs/kit\"\n\nexport fun"
  },
  {
    "path": "examples/client/sveltekit/src/routes/+page.server.ts",
    "chars": 88,
    "preview": "export async function load(event) {\n  return {\n    subject: event.locals.session,\n  }\n}\n"
  },
  {
    "path": "examples/client/sveltekit/src/routes/+page.svelte",
    "chars": 94,
    "preview": "<script lang=\"ts\">\n    const { data } = $props();\n</script>\n\n<h1>Hello {data.subject.id}</h1>\n"
  },
  {
    "path": "examples/client/sveltekit/src/routes/callback/+server.ts",
    "chars": 486,
    "preview": "import { redirect } from \"@sveltejs/kit\"\nimport { createAuthClient, setTokens } from \"$lib/auth.server.js\"\n\nexport async"
  },
  {
    "path": "examples/client/sveltekit/svelte.config.js",
    "chars": 673,
    "preview": "import adapter from \"@sveltejs/adapter-auto\"\nimport { vitePreprocess } from \"@sveltejs/vite-plugin-svelte\"\n\n/** @type {i"
  },
  {
    "path": "examples/client/sveltekit/tsconfig.json",
    "chars": 675,
    "preview": "{\n  \"extends\": \"./.svelte-kit/tsconfig.json\",\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"esMo"
  },
  {
    "path": "examples/client/sveltekit/vite.config.ts",
    "chars": 143,
    "preview": "import { sveltekit } from \"@sveltejs/kit/vite\"\nimport { defineConfig } from \"vite\"\n\nexport default defineConfig({\n  plug"
  },
  {
    "path": "examples/issuer/bun/.gitignore",
    "chars": 13,
    "preview": "persist.json\n"
  },
  {
    "path": "examples/issuer/bun/issuer.ts",
    "chars": 1094,
    "preview": "import { issuer } from \"@openauthjs/openauth\"\nimport { MemoryStorage } from \"@openauthjs/openauth/storage/memory\"\nimport"
  },
  {
    "path": "examples/issuer/bun/package.json",
    "chars": 136,
    "preview": "{\n  \"name\": \"@openauthjs/example-issuer-bun\",\n  \"version\": \"0.0.0\",\n  \"dependencies\": {\n    \"@openauthjs/openauth\": \"wor"
  },
  {
    "path": "examples/issuer/cloudflare/issuer.ts",
    "chars": 1251,
    "preview": "import { issuer } from \"@openauthjs/openauth\"\nimport { CloudflareStorage } from \"@openauthjs/openauth/storage/cloudflare"
  },
  {
    "path": "examples/issuer/cloudflare/package.json",
    "chars": 163,
    "preview": "{\n  \"name\": \"@openauthjs/example-issuer-cloudflare\",\n  \"version\": \"0.0.0\",\n  \"dependencies\": {\n    \"@openauthjs/openauth"
  },
  {
    "path": "examples/issuer/cloudflare/sst-env.d.ts",
    "chars": 350,
    "preview": "/* This file is auto-generated by SST. Do not edit. */\n/* tslint:disable */\n/* eslint-disable */\n/* deno-fmt-ignore-file"
  },
  {
    "path": "examples/issuer/cloudflare/sst.config.ts",
    "chars": 529,
    "preview": "/// <reference path=\"./.sst/platform/config.d.ts\" />\nexport default $config({\n  app(input) {\n    return {\n      name: \"o"
  },
  {
    "path": "examples/issuer/custom-frontend/auth/issuer.ts",
    "chars": 1154,
    "preview": "import { issuer } from \"@openauthjs/openauth\"\nimport { MemoryStorage } from \"@openauthjs/openauth/storage/memory\"\nimport"
  },
  {
    "path": "examples/issuer/custom-frontend/auth/package.json",
    "chars": 148,
    "preview": "{\n  \"name\": \"@openauthjs/example-custom-frontend-issuer\",\n  \"version\": \"0.0.0\",\n  \"dependencies\": {\n    \"@openauthjs/ope"
  },
  {
    "path": "examples/issuer/custom-frontend/frontend/frontend.tsx",
    "chars": 1447,
    "preview": "/** @jsx jsx */\n/** @jsxImportSource hono/jsx */\n\nimport { Hono } from \"hono\"\nimport { PropsWithChildren } from \"hono/js"
  },
  {
    "path": "examples/issuer/custom-frontend/frontend/package.json",
    "chars": 112,
    "preview": "{\n  \"name\": \"@openauthjs/example-custom-frontend\",\n  \"scripts\": {\n    \"dev\": \"bun run --hot frontend.tsx\"\n  }\n}\n"
  },
  {
    "path": "examples/issuer/custom-frontend/package.json",
    "chars": 73,
    "preview": "{\n  \"name\": \"custom-frontend\",\n  \"version\": \"0.0.0\",\n  \"private\": true\n}\n"
  },
  {
    "path": "examples/issuer/lambda/issuer.ts",
    "chars": 849,
    "preview": "import { issuer } from \"@openauthjs/openauth\"\nimport { handle } from \"hono/aws-lambda\"\nimport { subjects } from \"../../s"
  },
  {
    "path": "examples/issuer/lambda/package.json",
    "chars": 156,
    "preview": "{\n  \"name\": \"@openauthjs/example-issuer-aws\",\n  \"version\": \"0.0.0\",\n  \"dependencies\": {\n    \"@openauthjs/openauth\": \"wor"
  },
  {
    "path": "examples/issuer/lambda/sst-env.d.ts",
    "chars": 203,
    "preview": "/* This file is auto-generated by SST. Do not edit. */\n/* tslint:disable */\n/* eslint-disable */\n/* deno-fmt-ignore-file"
  },
  {
    "path": "examples/issuer/lambda/sst.config.ts",
    "chars": 352,
    "preview": "/// <reference path=\"./.sst/platform/config.d.ts\" />\nexport default $config({\n  app(input) {\n    return {\n      name: \"o"
  },
  {
    "path": "examples/issuer/node/.gitignore",
    "chars": 13,
    "preview": "persist.json\n"
  },
  {
    "path": "examples/issuer/node/authorizer.ts",
    "chars": 941,
    "preview": "import { issuer } from \"@openauthjs/openauth\"\nimport { MemoryStorage } from \"@openauthjs/openauth/storage/memory\"\nimport"
  },
  {
    "path": "examples/issuer/node/package.json",
    "chars": 62,
    "preview": "{\n  \"name\": \"node\",\n  \"version\": \"0.0.0\",\n  \"private\": true\n}\n"
  },
  {
    "path": "examples/quickstart/sst/.gitignore",
    "chars": 516,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "examples/quickstart/sst/README.md",
    "chars": 1450,
    "preview": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-re"
  },
  {
    "path": "examples/quickstart/sst/app/actions.ts",
    "chars": 1584,
    "preview": "\"use server\"\n\nimport { redirect } from \"next/navigation\"\nimport { headers as getHeaders, cookies as getCookies } from \"n"
  },
  {
    "path": "examples/quickstart/sst/app/api/callback/route.ts",
    "chars": 511,
    "preview": "import { client, setTokens } from \"../../auth\"\nimport { type NextRequest, NextResponse } from \"next/server\"\n\nexport asyn"
  },
  {
    "path": "examples/quickstart/sst/app/auth.ts",
    "chars": 635,
    "preview": "import { Resource } from \"sst\"\nimport { createClient } from \"@openauthjs/openauth/client\"\nimport { cookies as getCookies"
  },
  {
    "path": "examples/quickstart/sst/app/globals.css",
    "chars": 608,
    "preview": ":root {\n  --background: #ffffff;\n  --foreground: #171717;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --backg"
  },
  {
    "path": "examples/quickstart/sst/app/layout.tsx",
    "chars": 654,
    "preview": "import type { Metadata } from \"next\"\nimport { Geist, Geist_Mono } from \"next/font/google\"\nimport \"./globals.css\"\n\nconst "
  },
  {
    "path": "examples/quickstart/sst/app/page.module.css",
    "chars": 3314,
    "preview": ".page {\n  --gray-rgb: 0, 0, 0;\n  --gray-alpha-200: rgba(var(--gray-rgb), 0.08);\n  --gray-alpha-100: rgba(var(--gray-rgb)"
  },
  {
    "path": "examples/quickstart/sst/app/page.tsx",
    "chars": 1349,
    "preview": "import Image from \"next/image\"\nimport styles from \"./page.module.css\"\nimport { auth, login, logout } from \"./actions\"\n\ne"
  },
  {
    "path": "examples/quickstart/sst/auth/index.ts",
    "chars": 965,
    "preview": "import { handle } from \"hono/aws-lambda\"\nimport { issuer } from \"@openauthjs/openauth\"\nimport { CodeUI } from \"@openauth"
  },
  {
    "path": "examples/quickstart/sst/auth/subjects.ts",
    "chars": 189,
    "preview": "import { object, string } from \"valibot\"\nimport { createSubjects } from \"@openauthjs/openauth/subject\"\n\nexport const sub"
  },
  {
    "path": "examples/quickstart/sst/next.config.ts",
    "chars": 130,
    "preview": "import type { NextConfig } from \"next\"\n\nconst nextConfig: NextConfig = {\n  /* config options here */\n}\n\nexport default n"
  },
  {
    "path": "examples/quickstart/sst/package.json",
    "chars": 536,
    "preview": "{\n  \"name\": \"oa-nextjs\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"next build\",\n    \"dev\": \""
  },
  {
    "path": "examples/quickstart/sst/sst-env.d.ts",
    "chars": 337,
    "preview": "/* This file is auto-generated by SST. Do not edit. */\n/* tslint:disable */\n/* eslint-disable */\n/* deno-fmt-ignore-file"
  },
  {
    "path": "examples/quickstart/sst/sst.config.ts",
    "chars": 459,
    "preview": "/// <reference path=\"./.sst/platform/config.d.ts\" />\n\nexport default $config({\n  app(input) {\n    return {\n      name: \""
  },
  {
    "path": "examples/quickstart/sst/tsconfig.json",
    "chars": 615,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    "
  },
  {
    "path": "examples/quickstart/standalone/.gitignore",
    "chars": 480,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "examples/quickstart/standalone/README.md",
    "chars": 1450,
    "preview": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-re"
  },
  {
    "path": "examples/quickstart/standalone/app/actions.ts",
    "chars": 1584,
    "preview": "\"use server\"\n\nimport { redirect } from \"next/navigation\"\nimport { headers as getHeaders, cookies as getCookies } from \"n"
  },
  {
    "path": "examples/quickstart/standalone/app/api/callback/route.ts",
    "chars": 511,
    "preview": "import { client, setTokens } from \"../../auth\"\nimport { type NextRequest, NextResponse } from \"next/server\"\n\nexport asyn"
  },
  {
    "path": "examples/quickstart/standalone/app/auth.ts",
    "chars": 608,
    "preview": "import { createClient } from \"@openauthjs/openauth/client\"\nimport { cookies as getCookies } from \"next/headers\"\n\nexport "
  },
  {
    "path": "examples/quickstart/standalone/app/globals.css",
    "chars": 608,
    "preview": ":root {\n  --background: #ffffff;\n  --foreground: #171717;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --backg"
  },
  {
    "path": "examples/quickstart/standalone/app/layout.tsx",
    "chars": 654,
    "preview": "import type { Metadata } from \"next\"\nimport { Geist, Geist_Mono } from \"next/font/google\"\nimport \"./globals.css\"\n\nconst "
  },
  {
    "path": "examples/quickstart/standalone/app/page.module.css",
    "chars": 3314,
    "preview": ".page {\n  --gray-rgb: 0, 0, 0;\n  --gray-alpha-200: rgba(var(--gray-rgb), 0.08);\n  --gray-alpha-100: rgba(var(--gray-rgb)"
  },
  {
    "path": "examples/quickstart/standalone/app/page.tsx",
    "chars": 1349,
    "preview": "import Image from \"next/image\"\nimport styles from \"./page.module.css\"\nimport { auth, login, logout } from \"./actions\"\n\ne"
  },
  {
    "path": "examples/quickstart/standalone/auth/index.ts",
    "chars": 824,
    "preview": "import { issuer } from \"@openauthjs/openauth\"\nimport { CodeUI } from \"@openauthjs/openauth/ui/code\"\nimport { CodeProvide"
  },
  {
    "path": "examples/quickstart/standalone/auth/subjects.ts",
    "chars": 189,
    "preview": "import { object, string } from \"valibot\"\nimport { createSubjects } from \"@openauthjs/openauth/subject\"\n\nexport const sub"
  },
  {
    "path": "examples/quickstart/standalone/next.config.ts",
    "chars": 130,
    "preview": "import type { NextConfig } from \"next\"\n\nconst nextConfig: NextConfig = {\n  /* config options here */\n}\n\nexport default n"
  },
  {
    "path": "examples/quickstart/standalone/package.json",
    "chars": 549,
    "preview": "{\n  \"name\": \"oa-nextjs\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"dev:auth\": "
  },
  {
    "path": "examples/quickstart/standalone/tsconfig.json",
    "chars": 598,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    "
  },
  {
    "path": "examples/subjects.ts",
    "chars": 189,
    "preview": "import { object, string } from \"valibot\"\nimport { createSubjects } from \"@openauthjs/openauth/subject\"\n\nexport const sub"
  },
  {
    "path": "examples/tsconfig.json",
    "chars": 217,
    "preview": "{\n  \"extends\": \"@tsconfig/node22/tsconfig.json\",\n  \"compilerOptions\": {\n    \"module\": \"NodeNext\",\n    \"moduleResolution\""
  },
  {
    "path": "package.json",
    "chars": 492,
    "preview": "{\n  \"name\": \"openauthjs\",\n  \"module\": \"index.ts\",\n  \"type\": \"module\",\n  \"workspaces\": [\n    \"packages/openauth\",\n    \"ex"
  },
  {
    "path": "packages/openauth/CHANGELOG.md",
    "chars": 6262,
    "preview": "# @openauthjs/openauth\n\n## 0.4.3\n\n### Patch Changes\n\n- ec8ca65: include expires_in for refresh response\n\n## 0.4.2\n\n### P"
  },
  {
    "path": "packages/openauth/bunfig.toml",
    "chars": 23,
    "preview": "[test]\nroot = \"./test\"\n"
  },
  {
    "path": "packages/openauth/package.json",
    "chars": 976,
    "preview": "{\n  \"name\": \"@openauthjs/openauth\",\n  \"version\": \"0.4.3\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"bun run scrip"
  },
  {
    "path": "packages/openauth/script/build.ts",
    "chars": 604,
    "preview": "import { Glob, $ } from \"bun\"\nimport pkg from \"../package.json\"\n\nawait $`rm -rf dist`\nconst files = new Glob(\"./src/**/*"
  },
  {
    "path": "packages/openauth/src/client.ts",
    "chars": 19188,
    "preview": "/**\n * Use the OpenAuth client kick off your OAuth flows, exchange tokens, refresh tokens,\n * and verify tokens.\n *\n * F"
  },
  {
    "path": "packages/openauth/src/css.d.ts",
    "chars": 68,
    "preview": "declare module \"*.css\" {\n  const css: string\n  export default css\n}\n"
  },
  {
    "path": "packages/openauth/src/error.ts",
    "chars": 2677,
    "preview": "/**\n * A list of errors that can be thrown by OpenAuth.\n *\n * You can use these errors to check the type of error and ha"
  },
  {
    "path": "packages/openauth/src/index.ts",
    "chars": 563,
    "preview": "export {\n  /**\n   * @deprecated\n   * Use `import { createClient } from \"@openauthjs/openauth/client\"` instead - it will "
  },
  {
    "path": "packages/openauth/src/issuer.ts",
    "chars": 31635,
    "preview": "/**\n * The `issuer` create an OpentAuth server, a [Hono](https://hono.dev) app that's\n * designed to run anywhere.\n *\n *"
  },
  {
    "path": "packages/openauth/src/jwt.ts",
    "chars": 436,
    "preview": "import { JWTPayload, jwtVerify, KeyLike, SignJWT } from \"jose\"\n\nexport namespace jwt {\n  export function create(\n    pay"
  },
  {
    "path": "packages/openauth/src/keys.ts",
    "chars": 3870,
    "preview": "import {\n  exportJWK,\n  exportPKCS8,\n  exportSPKI,\n  generateKeyPair,\n  importPKCS8,\n  importSPKI,\n  JWK,\n  KeyLike,\n} f"
  },
  {
    "path": "packages/openauth/src/pkce.ts",
    "chars": 1128,
    "preview": "import { base64url } from \"jose\"\n\nfunction generateVerifier(length: number): string {\n  const buffer = new Uint8Array(le"
  },
  {
    "path": "packages/openauth/src/provider/apple.ts",
    "chars": 3075,
    "preview": "/**\n * Use this provider to authenticate with Apple. Supports both OAuth2 and OIDC.\n *\n * #### Using OAuth\n *\n * ```ts {"
  },
  {
    "path": "packages/openauth/src/provider/arctic.ts",
    "chars": 2016,
    "preview": "import type { OAuth2Tokens } from \"arctic\"\nimport { Context } from \"hono\"\nimport { Provider } from \"./provider.js\"\nimpor"
  },
  {
    "path": "packages/openauth/src/provider/code.ts",
    "chars": 5710,
    "preview": "/**\n * Configures a provider that supports pin code authentication. This is usually paired with the\n * `CodeUI`.\n *\n * `"
  },
  {
    "path": "packages/openauth/src/provider/cognito.ts",
    "chars": 1577,
    "preview": "/**\n * Use this provider to authenticate with a Cognito OAuth endpoint.\n *\n * ```ts {5-10}\n * import { CognitoProvider }"
  },
  {
    "path": "packages/openauth/src/provider/discord.ts",
    "chars": 967,
    "preview": "/**\n * Use this provider to authenticate with Discord.\n *\n * ```ts {5-8}\n * import { DiscordProvider } from \"@openauthjs"
  },
  {
    "path": "packages/openauth/src/provider/facebook.ts",
    "chars": 1885,
    "preview": "/**\n * Use this provider to authenticate with Facebook. Supports both OAuth2 and OIDC.\n *\n * #### Using OAuth\n *\n * ```t"
  },
  {
    "path": "packages/openauth/src/provider/github.ts",
    "chars": 967,
    "preview": "/**\n * Use this provider to authenticate with Github.\n *\n * ```ts {5-8}\n * import { GithubProvider } from \"@openauthjs/o"
  },
  {
    "path": "packages/openauth/src/provider/google.ts",
    "chars": 1887,
    "preview": "/**\n * Use this provider to authenticate with Google. Supports both OAuth2 and OIDC.\n *\n * #### Using OAuth\n *\n * ```ts "
  },
  {
    "path": "packages/openauth/src/provider/index.ts",
    "chars": 113,
    "preview": "export * from \"./code.js\"\nexport type { Provider as Provider } from \"./provider.js\"\nexport * from \"./spotify.js\"\n"
  },
  {
    "path": "packages/openauth/src/provider/jumpcloud.ts",
    "chars": 1002,
    "preview": "/**\n * Use this provider to authenticate with JumpCloud.\n *\n * ```ts {5-8}\n * import { JumpCloudProvider } from \"@openau"
  },
  {
    "path": "packages/openauth/src/provider/keycloak.ts",
    "chars": 1687,
    "preview": "/**\n * Use this provider to authenticate with a Keycloak server.\n *\n * ```ts {5-10}\n * import { KeycloakProvider } from "
  },
  {
    "path": "packages/openauth/src/provider/linkedin.ts",
    "chars": 362,
    "preview": "import { Oauth2Provider, type Oauth2WrappedConfig } from \"./oauth2.js\"\n\nexport function LinkedInAdapter(config: Oauth2Wr"
  },
  {
    "path": "packages/openauth/src/provider/microsoft.ts",
    "chars": 2236,
    "preview": "/**\n * Use this provider to authenticate with Microsoft. Supports both OAuth2 and OIDC.\n *\n * #### Using OAuth\n *\n * ```"
  },
  {
    "path": "packages/openauth/src/provider/oauth2.ts",
    "chars": 7474,
    "preview": "/**\n * Use this to connect authentication providers that support OAuth 2.0.\n *\n * ```ts {5-12}\n * import { Oauth2Provide"
  },
  {
    "path": "packages/openauth/src/provider/oidc.ts",
    "chars": 4585,
    "preview": "/**\n * Use this to connect authentication providers that support OIDC.\n *\n * ```ts {5-8}\n * import { OidcProvider } from"
  },
  {
    "path": "packages/openauth/src/provider/password.ts",
    "chars": 19224,
    "preview": "/**\n * Configures a provider that supports username and password authentication. This is usually\n * paired with the `Pas"
  },
  {
    "path": "packages/openauth/src/provider/provider.ts",
    "chars": 1048,
    "preview": "import type { Context, Hono } from \"hono\"\nimport { StorageAdapter } from \"../storage/storage.js\"\n\nexport type ProviderRo"
  },
  {
    "path": "packages/openauth/src/provider/slack.ts",
    "chars": 1792,
    "preview": "/**\n * Use this provider to authenticate with Slack.\n *\n * ```ts {5-10}\n * import { SlackProvider } from \"@openauthjs/op"
  },
  {
    "path": "packages/openauth/src/provider/spotify.ts",
    "chars": 976,
    "preview": "/**\n * Use this provider to authenticate with Spotify.\n *\n * ```ts {5-8}\n * import { SpotifyProvider } from \"@openauthjs"
  },
  {
    "path": "packages/openauth/src/provider/twitch.ts",
    "chars": 954,
    "preview": "/**\n * Use this provider to authenticate with Twitch.\n *\n * ```ts {5-8}\n * import { TwitchProvider } from \"@openauthjs/o"
  },
  {
    "path": "packages/openauth/src/provider/x.ts",
    "chars": 939,
    "preview": "/**\n * Use this provider to authenticate with X.com.\n *\n * ```ts {5-8}\n * import { XProvider } from \"@openauthjs/openaut"
  },
  {
    "path": "packages/openauth/src/provider/yahoo.ts",
    "chars": 964,
    "preview": "/**\n * Use this provider to authenticate with Yahoo.\n *\n * ```ts {5-8}\n * import { YahooProvider } from \"@openauthjs/ope"
  },
  {
    "path": "packages/openauth/src/random.ts",
    "chars": 662,
    "preview": "import { timingSafeEqual } from \"node:crypto\"\n\nexport function generateUnbiasedDigits(length: number): string {\n  const "
  },
  {
    "path": "packages/openauth/src/storage/aws.ts",
    "chars": 1663,
    "preview": "import { AwsClient } from \"aws4fetch\"\n\ninterface EC2Credentials {\n  AccessKeyId: string\n  SecretAccessKey: string\n  Toke"
  },
  {
    "path": "packages/openauth/src/storage/cloudflare.ts",
    "chars": 1933,
    "preview": "/**\n * Configure OpenAuth to use [Cloudflare KV](https://developers.cloudflare.com/kv/) as a\n * storage adapter.\n *\n * `"
  },
  {
    "path": "packages/openauth/src/storage/dynamo.ts",
    "chars": 4671,
    "preview": "/**\n * Configure OpenAuth to use [DynamoDB](https://aws.amazon.com/dynamodb/) as a storage adapter.\n *\n * ```ts\n * impor"
  },
  {
    "path": "packages/openauth/src/storage/memory.ts",
    "chars": 3360,
    "preview": "/**\n * Configure OpenAuth to use a simple in-memory store.\n *\n * :::caution\n * This is not meant to be used in productio"
  },
  {
    "path": "packages/openauth/src/storage/storage.ts",
    "chars": 1203,
    "preview": "export interface StorageAdapter {\n  get(key: string[]): Promise<Record<string, any> | undefined>\n  remove(key: string[])"
  },
  {
    "path": "packages/openauth/src/subject.ts",
    "chars": 3536,
    "preview": "/**\n * Subjects are what the access token generated at the end of the auth flow will map to. Under\n * the hood, the acce"
  },
  {
    "path": "packages/openauth/src/ui/base.tsx",
    "chars": 4095,
    "preview": "import { PropsWithChildren } from \"hono/jsx\"\nimport css from \"./ui.css\" assert { type: \"text\" }\nimport { getTheme } from"
  },
  {
    "path": "packages/openauth/src/ui/code.tsx",
    "chars": 5699,
    "preview": "/**\n * Configure the UI that's used by the Code provider.\n *\n * ```ts {1,7-12}\n * import { CodeUI } from \"@openauthjs/op"
  },
  {
    "path": "packages/openauth/src/ui/form.tsx",
    "chars": 1046,
    "preview": "/** @jsxImportSource hono/jsx */\n\nexport function FormAlert(props: {\n  message?: string\n  color?: \"danger\" | \"success\"\n}"
  },
  {
    "path": "packages/openauth/src/ui/icon.tsx",
    "chars": 6067,
    "preview": "/** @jsxImportSource hono/jsx */\n\nexport const ICON_GITHUB = (\n  <svg\n    viewBox=\"0 0 256 250\"\n    width=\"256\"\n    heig"
  },
  {
    "path": "packages/openauth/src/ui/password.tsx",
    "chars": 11815,
    "preview": "/**\n * Configure the UI that's used by the Password provider.\n *\n * ```ts {1,7-12}\n * import { PasswordUI } from \"@opena"
  },
  {
    "path": "packages/openauth/src/ui/select.tsx",
    "chars": 7775,
    "preview": "/**\n * The UI that's displayed when loading the root page of the OpenAuth server. You can configure\n * which providers s"
  },
  {
    "path": "packages/openauth/src/ui/theme.ts",
    "chars": 6251,
    "preview": "/**\n * Use one of the built-in themes.\n *\n * @example\n *\n * ```ts\n * import { THEME_SST } from \"@openauthjs/openauth/ui/"
  },
  {
    "path": "packages/openauth/src/ui/ui.css",
    "chars": 6331,
    "preview": "@import url(\"https://unpkg.com/tailwindcss@3.4.15/src/css/preflight.css\");\n\n:root {\n  --color-background-dark: #0e0e11;\n"
  },
  {
    "path": "packages/openauth/src/util.ts",
    "chars": 1316,
    "preview": "import type { Context } from \"hono\"\n\nexport type Prettify<T> = {\n  [K in keyof T]: T[K]\n}\n\nexport function getRelativeUr"
  },
  {
    "path": "packages/openauth/test/client.test.ts",
    "chars": 3836,
    "preview": "import {\n  expect,\n  test,\n  setSystemTime,\n  describe,\n  beforeEach,\n  afterEach,\n  spyOn,\n  afterAll,\n  mock,\n} from \""
  },
  {
    "path": "packages/openauth/test/issuer.test.ts",
    "chars": 11360,
    "preview": "import {\n  expect,\n  test,\n  setSystemTime,\n  describe,\n  beforeEach,\n  afterEach,\n} from \"bun:test\"\nimport { object, st"
  },
  {
    "path": "packages/openauth/test/scrap.test.ts",
    "chars": 2659,
    "preview": "import { expect, test } from \"bun:test\"\nimport { issuer } from \"../src/issuer.js\"\nimport { MemoryStorage } from \"../src/"
  },
  {
    "path": "packages/openauth/test/storage.test.ts",
    "chars": 2729,
    "preview": "import { afterEach, setSystemTime } from \"bun:test\"\nimport { beforeEach, describe, expect, test } from \"bun:test\"\nimport"
  },
  {
    "path": "packages/openauth/test/util.test.ts",
    "chars": 3893,
    "preview": "import { expect, test } from \"bun:test\"\nimport { Context } from \"hono\"\nimport { getRelativeUrl, isDomainMatch } from \".."
  },
  {
    "path": "packages/openauth/tsconfig.json",
    "chars": 293,
    "preview": "{\n  \"extends\": \"@tsconfig/node22/tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"declaration\": true,\n "
  },
  {
    "path": "scripts/format",
    "chars": 280,
    "preview": "#!/usr/bin/env bash\n\nset -e\n\nbun x prettier --write \"**/*.{js,jsx,ts,tsx,json,md,yaml,yml}\"\n\n# Check for changes\nif ! gi"
  },
  {
    "path": "www/.gitignore",
    "chars": 249,
    "preview": "# build output\ndist/\n# generated types\n.astro/\n# doc output\noutput/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*"
  },
  {
    "path": "www/.vscode/extensions.json",
    "chars": 87,
    "preview": "{\n  \"recommendations\": [\"astro-build.astro-vscode\"],\n  \"unwantedRecommendations\": []\n}\n"
  },
  {
    "path": "www/.vscode/launch.json",
    "chars": 207,
    "preview": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"command\": \"./node_modules/.bin/astro dev\",\n      \"name\": \"Dev"
  },
  {
    "path": "www/README.md",
    "chars": 2564,
    "preview": "# Starlight Starter Kit: Basics\n\n[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https"
  },
  {
    "path": "www/astro.config.mjs",
    "chars": 4063,
    "preview": "import theme from \"toolbeam-docs-theme\"\nimport starlight from \"@astrojs/starlight\"\nimport { defineConfig } from \"astro/c"
  },
  {
    "path": "www/config.ts",
    "chars": 107,
    "preview": "export default {\n  github: \"https://github.com/toolbeam/openauth\",\n  discord: \"https://sst.dev/discord\",\n}\n"
  },
  {
    "path": "www/generate.ts",
    "chars": 30208,
    "preview": "import * as path from \"path\"\nimport * as fs from \"fs\"\nimport * as TypeDoc from \"typedoc\"\nimport config from \"./config\"\n\n"
  },
  {
    "path": "www/package.json",
    "chars": 557,
    "preview": "{\n  \"name\": \"www\",\n  \"type\": \"module\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"dev\": \"astro dev\",\n    \"start\": \"astro "
  },
  {
    "path": "www/src/components/Hero.astro",
    "chars": 260,
    "preview": "---\nimport Default from '@astrojs/starlight/components/Hero.astro';\nimport Lander from './Lander.astro';\n\nconst { slug }"
  },
  {
    "path": "www/src/components/Lander.astro",
    "chars": 4137,
    "preview": "---\nimport { Image } from 'astro:assets';\nimport config from \"virtual:starlight/user-config\";\nimport type { Props } from"
  },
  {
    "path": "www/src/content/config.ts",
    "chars": 188,
    "preview": "import { defineCollection } from \"astro:content\"\nimport { docsSchema } from \"@astrojs/starlight/schema\"\n\nexport const co"
  },
  {
    "path": "www/src/content/docs/docs/client.mdx",
    "chars": 20935,
    "preview": "---\ntitle: Client\neditUrl: https://github.com/toolbeam/openauth/blob/master/packages/openauth/src/client.ts\ndescription:"
  },
  {
    "path": "www/src/content/docs/docs/index.mdx",
    "chars": 12858,
    "preview": "---\ntitle: OpenAuth\ndescription: Introduction to OpenAuth.\n---\n\nimport { Image } from \"astro:assets\"\nimport { Tabs, TabI"
  }
]

// ... and 37 more files (download for full content)

About this extraction

This page contains the full source code of the sst/openauth GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 237 files (577.6 KB), approximately 164.3k tokens, and a symbol index with 279 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!