Compare commits

...

33 Commits

Author SHA1 Message Date
Balázs Orbán
6590993fdc chore(release): bump package version(s) [skip ci] 2022-07-21 16:35:03 +02:00
Balázs Orbán
0ea96796b2 fix: improve logger (#4970)
* fix: add debug warning, only show warnings once

* fix: prefer `debug` for details

* remove url

* test: fix tests

* Update docs/docs/errors.md

Co-authored-by: Thang Vu <31528554+ThangHuuVu@users.noreply.github.com>

* Update callback.ts

Co-authored-by: Thang Vu <31528554+ThangHuuVu@users.noreply.github.com>
2022-07-21 16:00:16 +02:00
Misha Kaletsky
8ec940bd6a docs: highlight archiving of next-auth/react-query (#4964)
* docs: highlight archiving of next-auth/react-query

project is read-only and author said users should just copy-paste the implementation: https://github.com/nextauthjs/react-query/issues/7#issuecomment-923099050

* Update docs/docs/getting-started/client.md

Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Update docs/docs/getting-started/client.md

Co-authored-by: Balázs Orbán <info@balazsorban.com>

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-07-21 13:59:46 +02:00
Balázs Orbán
e3bcdf83f1 chore: update lock file 2022-07-20 03:18:02 +02:00
Balázs Orbán
4084297334 chore(release): extract release script to its own package 2022-07-20 03:08:03 +02:00
Balázs Orbán
c9827960b1 chore(release): read packages dynamically 2022-07-20 00:36:49 +02:00
Balázs Orbán
946a825865 chore: fix adapters PR auto-labeler 2022-07-19 23:55:03 +02:00
Balázs Orbán
c57d8c997e fix(adapters): set correct peer dependency version of next-auth (#4950)
* fix(adapters): set correct peer dependency version of `next-auth`

* fix fauna peer dependency
2022-07-19 23:46:45 +02:00
Balázs Orbán
e2b92bf04f chore: add newlines to PR comment 2022-07-19 17:45:55 +02:00
Balázs Orbán
8bff050e4e chore(release): bump version [skip ci] 2022-07-19 17:30:40 +02:00
Thang Vu
1a79a1a612 docs: FAQ framework-agnostic & session sharing (#4962)
Split a FAQ into two parts:
Before:
- Can I use NextAuth.js with a website that does not use Next.js?

After:
- Can I use NextAuth.js with a framework different than Next.js?
- Can session generated by NextAuth.js be used by another website?
2022-07-19 17:29:54 +02:00
Balázs Orbán
b7065a602f chore: correct Middleware logic in Next.js example 2022-07-19 17:16:51 +02:00
Balázs Orbán
61b92ec1b6 chore: revert type assertion 2022-07-19 16:57:16 +02:00
Balázs Orbán
282f7ab340 fix(ts): fix Middleware internal type 2022-07-19 16:46:13 +02:00
Balázs Orbán
4f56e414b0 chore: simplify dev app 2022-07-19 16:42:21 +02:00
Yoann Fleury
2725d07eb7 fix(providers): migrate GitLab provider to TS (#4929) 2022-07-17 04:47:15 +02:00
Balázs Orbán
5a8b029523 docs: clarify getToken + secret in example
ref #4954
2022-07-17 04:44:11 +02:00
Balázs Orbán
f62a985848 docs: clarify getToken and secret
Ref: #4954
2022-07-17 04:39:47 +02:00
Balázs Orbán
edd6fb5989 Merge branch 'main' of github.com:nextauthjs/next-auth 2022-07-17 04:29:49 +02:00
Balázs Orbán
fb60554a62 chore(ts): explicitly set next path in next-auth 2022-07-17 04:29:44 +02:00
cobbvanth
9784dfb631 docs: Remove import of "useSession" from wrong location (#4952)
This tutorial snipped erroneously imports useSession from "next-auth/next", when it actually resides in "next-auth/react".
2022-07-16 23:06:00 +02:00
Balázs Orbán
4ff836a8cf test(providers): add fetch polyfill to redis test runs 2022-07-16 17:39:32 +02:00
Balázs Orbán
042955eaaa fix(providers): allow issuer in Azure AD B2C 2022-07-16 14:20:31 +02:00
Balázs Orbán
82e107c0e7 chore: improve dev app DX 2022-07-16 14:19:33 +02:00
dependabot[bot]
f7050347e8 chore(deps-dev): bump svelte from 3.46.4 to 3.49.0 in /apps/playground-sveltekit (#4947)
chore(deps-dev): bump svelte in /apps/playground-sveltekit

Bumps [svelte](https://github.com/sveltejs/svelte) from 3.46.4 to 3.49.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/compare/v3.46.4...v3.49.0)

---
updated-dependencies:
- dependency-name: svelte
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-15 23:17:21 +02:00
Balázs Orbán
c56abbd745 chore: update CODEOWNERS (#4941)
* chore: update codeowners

* reorganize
2022-07-15 12:35:54 +02:00
Thang Vu
3f6d99e8df chore: add Thang to contributor (#4944) 2022-07-15 12:34:36 +02:00
Balázs Orbán
46eedee3c8 fix(ts): remove TS workaround for withAuth (#4926)
* fix(ts): improve Middleware types

* docs: remove TS workaround for Middleware

* ignore lint

* simplify
2022-07-15 04:39:15 +02:00
Balázs Orbán
bb664a27da fix(providers): typo in GitHub provider scope (#4938) 2022-07-15 04:38:42 +02:00
Balázs Orbán
a14fbea0b5 chore: add TODO comment for next major 2022-07-15 02:41:09 +02:00
Balázs Orbán
4705632c6b chore: add TODO comment for next major version 2022-07-15 02:38:59 +02:00
Balázs Orbán
2296471f02 chore: add pnpm to experimental release comment 2022-07-15 02:20:06 +02:00
S. Suzuki
8853000fd5 fix(ts): handleMiddleware return type can be NextMiddlewareResult (#4818)
Co-authored-by: Lluis Agusti <hi@llu.lu>
2022-07-14 00:20:19 +02:00
63 changed files with 562 additions and 1193 deletions

15
.github/CODEOWNERS vendored
View File

@@ -1,4 +1,11 @@
/types/ @balazsorban44 @lluia
/docs/ @balazsorban44 @ndom91
/adapters/ @balazsorban44 @ndom91
/__tests__/ @lluia
# Learn how to add code owners here:
# https://help.github.com/en/articles/about-code-owners
* @balazsorban44
.github @ThangHuuVu
/apps/ @lluia @ndom91 @ThangHuuVu
/docs/ @lluia @ndom91
/packages/ @ThangHuuVu
/packages/adapter-*/ @ndom91
/**/*test* @lluia
/**/*type* @lluia

View File

@@ -10,7 +10,7 @@ providers:
adapters:
- packages/next-auth/src/adapters.ts
- packages/*-adapter/**
- packages/adapter-*/**
dgraph:
- packages/adapter-dgraph/**

View File

@@ -4,5 +4,5 @@ outputs:
version:
description: "npm package version"
runs:
using: "node12"
using: "node16"
main: "index.js"

View File

@@ -108,7 +108,11 @@ jobs:
- name: Comment version on PR
uses: NejcZdovc/comment-pr@v1
with:
message: "🎉 Experimental release [published on npm](https://www.npmjs.com/package/next-auth/v/${{ env.VERSION }})!\n\n```sh\nnpm i next-auth@${{ env.VERSION }}\n```\n```sh\nyarn add next-auth@${{ env.VERSION }}\n```"
message:
"🎉 Experimental release [published 📦️ on npm](https://npmjs.com/package/next-auth/v/${{ env.VERSION }})!\n \
```sh\npnpm add next-auth@${{ env.VERSION }}\n```\n \
```sh\nyarn add next-auth@${{ env.VERSION }}\n```\n \
```sh\nnpm i next-auth@${{ env.VERSION }}\n```"
env:
VERSION: ${{ steps.determine-version.outputs.version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -47,9 +47,5 @@ EMAIL_FROM=user@gmail.com
# MongoDB: DATABASE_URL=mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true
DATABASE_URL=
BOXYHQSAML_ISSUER="https://jackson-demo.boxyhq.com"
BOXYHQSAML_ID="tenant=boxyhq.com&product=saml-demo.boxyhq.com"
BOXYHQSAML_SECRET="dummy"
WIKIMEDIA_ID=
WIKIMEDIA_SECRET=

View File

@@ -30,12 +30,11 @@ export const config = { matcher: ["/middleware-protected"] }
// export default withAuth(
// function middleware(req, ev) {
// console.log(req, ev)
// return undefined // NOTE: `NextMiddleware` should allow returning `void`
// },
// {
// callbacks: {
// authorized: ({ token }) => token.name === "Balázs Orbán",
// }
// },
// }
// )

View File

@@ -1,251 +1,134 @@
import NextAuth, { NextAuthOptions } from "next-auth"
// import EmailProvider from "next-auth/providers/email"
import GitHubProvider from "next-auth/providers/github"
import Auth0Provider from "next-auth/providers/auth0"
import KeycloakProvider from "next-auth/providers/keycloak"
import TwitterProvider, {
// TwitterLegacy as TwitterLegacyProvider,
} from "next-auth/providers/twitter"
import CredentialsProvider from "next-auth/providers/credentials"
import IDS4Provider from "next-auth/providers/identity-server4"
import DuendeIDS6Provider from "next-auth/providers/duende-identity-server6"
import Twitch from "next-auth/providers/twitch"
import GoogleProvider from "next-auth/providers/google"
import FacebookProvider from "next-auth/providers/facebook"
import FoursquareProvider from "next-auth/providers/foursquare"
// import FreshbooksProvider from "next-auth/providers/freshbooks"
import GitlabProvider from "next-auth/providers/gitlab"
import InstagramProvider from "next-auth/providers/instagram"
import LineProvider from "next-auth/providers/line"
import LinkedInProvider from "next-auth/providers/linkedin"
import MailchimpProvider from "next-auth/providers/mailchimp"
import DiscordProvider from "next-auth/providers/discord"
import AzureADProvider from "next-auth/providers/azure-ad"
import SpotifyProvider from "next-auth/providers/spotify"
import CognitoProvider from "next-auth/providers/cognito"
import SlackProvider from "next-auth/providers/slack"
import Okta from "next-auth/providers/okta"
import NextAuth from "next-auth"
import type { NextAuthOptions } from "next-auth"
// Providers
import Apple from "next-auth/providers/apple"
import Auth0 from "next-auth/providers/auth0"
import AzureAD from "next-auth/providers/azure-ad"
import AzureB2C from "next-auth/providers/azure-ad-b2c"
import OsuProvider from "next-auth/providers/osu"
import AppleProvider from "next-auth/providers/apple"
import PatreonProvider from "next-auth/providers/patreon"
import TraktProvider from "next-auth/providers/trakt"
import WorkOSProvider from "next-auth/providers/workos"
import BoxyHQSAMLProvider from "next-auth/providers/boxyhq-saml"
import WikimediaProvider from "next-auth/providers/wikimedia"
import VkProvider from "next-auth/providers/vk"
import BoxyHQSAML from "next-auth/providers/boxyhq-saml"
import Cognito from "next-auth/providers/cognito"
import Credentials from "next-auth/providers/credentials"
import Discord from "next-auth/providers/discord"
import DuendeIDS6 from "next-auth/providers/duende-identity-server6"
import Email from "next-auth/providers/email"
import Facebook from "next-auth/providers/facebook"
import Foursquare from "next-auth/providers/foursquare"
import Freshbooks from "next-auth/providers/freshbooks"
import GitHub from "next-auth/providers/github"
import Gitlab from "next-auth/providers/gitlab"
import Google from "next-auth/providers/google"
import IDS4 from "next-auth/providers/identity-server4"
import Instagram from "next-auth/providers/instagram"
import Keycloak from "next-auth/providers/keycloak"
import Line from "next-auth/providers/line"
import LinkedIn from "next-auth/providers/linkedin"
import Mailchimp from "next-auth/providers/mailchimp"
import Okta from "next-auth/providers/okta"
import Osu from "next-auth/providers/osu"
import Patreon from "next-auth/providers/patreon"
import Slack from "next-auth/providers/slack"
import Spotify from "next-auth/providers/spotify"
import Trakt from "next-auth/providers/trakt"
import Twitch from "next-auth/providers/twitch"
import Twitter, { TwitterLegacy } from "next-auth/providers/twitter"
import Vk from "next-auth/providers/vk"
import Wikimedia from "next-auth/providers/wikimedia"
import WorkOS from "next-auth/providers/workos"
// TypeORM
// import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
// const adapter = TypeORMLegacyAdapter({
// type: "sqlite",
// name: "next-auth-test-memory",
// database: "./typeorm/dev.db",
// synchronize: true,
// })
// Adapters
import { PrismaClient } from "@prisma/client"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { Client as FaunaClient } from "faunadb"
import { FaunaAdapter } from "@next-auth/fauna-adapter"
import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
// // Prisma
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
// import { PrismaClient } from "@prisma/client"
// const prisma = new PrismaClient()
// const adapter = PrismaAdapter(prisma)
// // Fauna
// import { Client as FaunaClient } from "faunadb"
// import { FaunaAdapter } from "@next-auth/fauna-adapter"
// const client = new FaunaClient({
// secret: process.env.FAUNA_SECRET,
// domain: process.env.FAUNA_DOMAIN,
// })
// const adapter = FaunaAdapter(client)
// // Dummy
// const adapter: any = {
// getUserByEmail: (email) => ({ id: "1", email, emailVerified: null }),
// createVerificationToken: (token) => token,
// }
export const authOptions: NextAuthOptions = {
// adapter,
providers: [
// E-mail
// Start fake e-mail server with `npm run start:email`
// EmailProvider({
// server: {
// host: "127.0.0.1",
// auth: null,
// secure: false,
// port: 1025,
// tls: { rejectUnauthorized: false },
// },
// }),
// Credentials
CredentialsProvider({
name: "Credentials",
credentials: {
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (credentials.password === "pw") {
return {
name: "Fill Murray",
email: "bill@fillmurray.com",
image: "https://www.fillmurray.com/64/64",
}
}
return null
},
}),
// OAuth 1
// TwitterLegacyProvider({
// clientId: process.env.TWITTER_LEGACY_ID,
// clientSecret: process.env.TWITTER_LEGACY_SECRET,
// }),
// OAuth 2 / OIDC
TwitterProvider({
// Opt-in to the new Twitter API for now. Should be default in the future.
version: "2.0",
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET,
}),
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Auth0Provider({
clientId: process.env.AUTH0_ID,
clientSecret: process.env.AUTH0_SECRET,
issuer: process.env.AUTH0_ISSUER,
}),
KeycloakProvider({
clientId: process.env.KEYCLOAK_ID,
clientSecret: process.env.KEYCLOAK_SECRET,
issuer: process.env.KEYCLOAK_ISSUER,
}),
Twitch({
clientId: process.env.TWITCH_ID,
clientSecret: process.env.TWITCH_SECRET,
}),
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
FacebookProvider({
clientId: process.env.FACEBOOK_ID,
clientSecret: process.env.FACEBOOK_SECRET,
}),
FoursquareProvider({
clientId: process.env.FOURSQUARE_ID,
clientSecret: process.env.FOURSQUARE_SECRET,
}),
// FreshbooksProvider({
// clientId: process.env.FRESHBOOKS_ID,
// clientSecret: process.env.FRESHBOOKS_SECRET,
// }),
GitlabProvider({
clientId: process.env.GITLAB_ID,
clientSecret: process.env.GITLAB_SECRET,
}),
InstagramProvider({
clientId: process.env.INSTAGRAM_ID,
clientSecret: process.env.INSTAGRAM_SECRET,
}),
LineProvider({
clientId: process.env.LINE_ID,
clientSecret: process.env.LINE_SECRET,
}),
LinkedInProvider({
clientId: process.env.LINKEDIN_ID,
clientSecret: process.env.LINKEDIN_SECRET,
}),
MailchimpProvider({
clientId: process.env.MAILCHIMP_ID,
clientSecret: process.env.MAILCHIMP_SECRET,
}),
IDS4Provider({
clientId: process.env.IDS4_ID,
clientSecret: process.env.IDS4_SECRET,
issuer: process.env.IDS4_ISSUER,
}),
DuendeIDS6Provider({
clientId: "interactive.confidential",
clientSecret: "secret",
issuer: "https://demo.duendesoftware.com",
}),
DiscordProvider({
clientId: process.env.DISCORD_ID,
clientSecret: process.env.DISCORD_SECRET,
}),
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
tenantId: process.env.AZURE_AD_TENANT_ID,
profilePhotoSize: 48,
}),
SpotifyProvider({
clientId: process.env.SPOTIFY_ID,
clientSecret: process.env.SPOTIFY_SECRET,
}),
CognitoProvider({
clientId: process.env.COGNITO_ID,
clientSecret: process.env.COGNITO_SECRET,
issuer: process.env.COGNITO_ISSUER,
}),
Okta({
clientId: process.env.OKTA_ID,
clientSecret: process.env.OKTA_SECRET,
issuer: process.env.OKTA_ISSUER,
}),
SlackProvider({
clientId: process.env.SLACK_ID,
clientSecret: process.env.SLACK_SECRET,
}),
AzureB2C({
clientId: process.env.AZURE_B2C_ID,
clientSecret: process.env.AZURE_B2C_SECRET,
tenantId: process.env.AZURE_B2C_TENANT_ID,
primaryUserFlow: process.env.AZURE_B2C_PRIMARY_USER_FLOW,
}),
OsuProvider({
clientId: process.env.OSU_CLIENT_ID,
clientSecret: process.env.OSU_CLIENT_SECRET,
}),
AppleProvider({
clientId: process.env.APPLE_ID,
clientSecret: process.env.APPLE_SECRET,
}),
PatreonProvider({
clientId: process.env.PATREON_ID,
clientSecret: process.env.PATREON_SECRET,
}),
TraktProvider({
clientId: process.env.TRAKT_ID,
clientSecret: process.env.TRAKT_SECRET,
}),
WorkOSProvider({
clientId: process.env.WORKOS_ID,
clientSecret: process.env.WORKOS_SECRET,
}),
BoxyHQSAMLProvider({
issuer: process.env.BOXYHQSAML_ISSUER ?? "https://example.com",
clientId: process.env.BOXYHQSAML_ID,
clientSecret: process.env.BOXYHQSAML_SECRET,
}),
WikimediaProvider({
clientId: process.env.WIKIMEDIA_ID,
clientSecret: process.env.WIKIMEDIA_SECRET,
}),
VkProvider({
clientId: process.env.VK_ID,
clientSecret: process.env.VK_SECRET
}),
],
debug: true,
theme: {
colorScheme: "auto",
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
brandColor: "#1786fb",
// Add an adapter you want to test here.
const adapters = {
prisma() {
const client = globalThis.prisma || new PrismaClient()
if (process.env.NODE_ENV !== "production") globalThis.prisma = client
return PrismaAdapter(client)
},
typeorm() {
return TypeORMLegacyAdapter({
type: "sqlite",
name: "next-auth-test-memory",
database: "./typeorm/dev.db",
synchronize: true,
})
},
fauna() {
const client =
globalThis.fauna ||
new FaunaClient({
secret: process.env.FAUNA_SECRET,
domain: process.env.FAUNA_DOMAIN,
})
if (process.env.NODE_ENV !== "production") global.fauna = client
return FaunaAdapter(client)
},
noop() {
return undefined
},
}
export const authOptions: NextAuthOptions = {
adapter: adapters.noop(),
debug: true,
theme: {
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
brandColor: "#1786fb",
},
providers: [
Credentials({
credentials: { password: { label: "Password", type: "password" } },
async authorize(credentials) {
if (credentials.password !== "pw") return null
return { name: "Fill Murray", email: "bill@fillmurray.com", image: "https://www.fillmurray.com/64/64" }
},
}),
Apple({ clientId: process.env.APPLE_ID, clientSecret: process.env.APPLE_SECRET }),
Auth0({ clientId: process.env.AUTH0_ID, clientSecret: process.env.AUTH0_SECRET, issuer: process.env.AUTH0_ISSUER }),
AzureAD({ clientId: process.env.AZURE_AD_CLIENT_ID, clientSecret: process.env.AZURE_AD_CLIENT_SECRET, tenantId: process.env.AZURE_AD_TENANT_ID }),
AzureB2C({ clientId: process.env.AZURE_B2C_ID, clientSecret: process.env.AZURE_B2C_SECRET, issuer: process.env.AZURE_B2C_ISSUER }),
BoxyHQSAML({ issuer: "https://jackson-demo.boxyhq.com", clientId: "tenant=boxyhq.com&product=saml-demo.boxyhq.com", clientSecret: "dummy" }),
Cognito({ clientId: process.env.COGNITO_ID, clientSecret: process.env.COGNITO_SECRET, issuer: process.env.COGNITO_ISSUER }),
Discord({ clientId: process.env.DISCORD_ID, clientSecret: process.env.DISCORD_SECRET }),
DuendeIDS6({ clientId: "interactive.confidential", clientSecret: "secret", issuer: "https://demo.duendesoftware.com" }),
Facebook({ clientId: process.env.FACEBOOK_ID, clientSecret: process.env.FACEBOOK_SECRET }),
Foursquare({ clientId: process.env.FOURSQUARE_ID, clientSecret: process.env.FOURSQUARE_SECRET }),
Freshbooks({ clientId: process.env.FRESHBOOKS_ID, clientSecret: process.env.FRESHBOOKS_SECRET }),
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),
Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }),
Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET }),
IDS4({ clientId: process.env.IDS4_ID, clientSecret: process.env.IDS4_SECRET, issuer: process.env.IDS4_ISSUER }),
Instagram({ clientId: process.env.INSTAGRAM_ID, clientSecret: process.env.INSTAGRAM_SECRET }),
Keycloak({ clientId: process.env.KEYCLOAK_ID, clientSecret: process.env.KEYCLOAK_SECRET, issuer: process.env.KEYCLOAK_ISSUER }),
Line({ clientId: process.env.LINE_ID, clientSecret: process.env.LINE_SECRET }),
LinkedIn({ clientId: process.env.LINKEDIN_ID, clientSecret: process.env.LINKEDIN_SECRET }),
Mailchimp({ clientId: process.env.MAILCHIMP_ID, clientSecret: process.env.MAILCHIMP_SECRET }),
Okta({ clientId: process.env.OKTA_ID, clientSecret: process.env.OKTA_SECRET, issuer: process.env.OKTA_ISSUER }),
Osu({ clientId: process.env.OSU_CLIENT_ID, clientSecret: process.env.OSU_CLIENT_SECRET }),
Patreon({ clientId: process.env.PATREON_ID, clientSecret: process.env.PATREON_SECRET }),
Slack({ clientId: process.env.SLACK_ID, clientSecret: process.env.SLACK_SECRET }),
Spotify({ clientId: process.env.SPOTIFY_ID, clientSecret: process.env.SPOTIFY_SECRET }),
Trakt({ clientId: process.env.TRAKT_ID, clientSecret: process.env.TRAKT_SECRET }),
Twitch({ clientId: process.env.TWITCH_ID, clientSecret: process.env.TWITCH_SECRET }),
Twitter({ version: "2.0", clientId: process.env.TWITTER_ID, clientSecret: process.env.TWITTER_SECRET }),
TwitterLegacy({ clientId: process.env.TWITTER_LEGACY_ID, clientSecret: process.env.TWITTER_LEGACY_SECRET }),
Vk({ clientId: process.env.VK_ID, clientSecret: process.env.VK_SECRET }),
Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }),
WorkOS({ clientId: process.env.WORKOS_ID, clientSecret: process.env.WORKOS_SECRET }),
],
}
if (authOptions.adapter) {
authOptions.providers.unshift(
// NOTE: You can start a fake e-mail server with `pnpm email`
// and then go to `http://localhost:1080` in the browser
Email({ server: "smtp://127.0.0.1:1025?tls.rejectUnauthorized=false" })
)
}
export default NextAuth(authOptions)

View File

@@ -2,6 +2,6 @@
import { getToken } from "next-auth/jwt"
export default async (req, res) => {
const token = await getToken({ req, secret: process.env.SECRET })
const token = await getToken({ req })
res.send(JSON.stringify(token, null, 2))
}

View File

@@ -3,9 +3,14 @@ import { withAuth } from "next-auth/middleware"
// More on how NextAuth.js middleware works: https://next-auth.js.org/configuration/nextjs#middleware
export default withAuth({
callbacks: {
authorized: ({ req, token }) =>
// /admin requires admin role, but /me only requires the user to be logged in.
req.nextUrl.pathname !== "/admin" || token?.userRole === "admin",
authorized({ req, token }) {
// `/admin` requires admin role
if (req.nextUrl.pathname === "/admin") {
return token?.userRole === "admin"
}
// `/me` only requires the user to be logged in
return !!token
},
},
})

View File

@@ -3,12 +3,12 @@ import { getToken } from "next-auth/jwt"
import type { NextApiRequest, NextApiResponse } from "next"
const secret = process.env.NEXTAUTH_SECRET
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const token = await getToken({ req, secret })
// If you don't have the NEXTAUTH_SECRET environment variable set,
// you will have to pass your secret as `secret` to `getToken`
const token = await getToken({ req })
res.send(JSON.stringify(token, null, 2))
}

View File

@@ -21,7 +21,7 @@
"eslint-plugin-svelte3": "^3.2.1",
"prettier": "^2.5.1",
"prettier-plugin-svelte": "^2.5.0",
"svelte": "^3.44.0",
"svelte": "^3.49.0",
"svelte-check": "^2.2.6",
"svelte-preprocess": "^4.10.1",
"tslib": "^2.3.1",

View File

@@ -1617,10 +1617,10 @@ svelte-preprocess@^4.0.0, svelte-preprocess@^4.10.1:
sorcery "^0.10.0"
strip-indent "^3.0.0"
svelte@^3.44.0:
version "3.46.4"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.46.4.tgz#0c46bc4a3e20a2617a1b7dc43a722f9d6c084a38"
integrity sha512-qKJzw6DpA33CIa+C/rGp4AUdSfii0DOTCzj/2YpSKKayw5WGSS624Et9L1nU1k2OVRS9vaENQXp2CVZNU+xvIg==
svelte@^3.49.0:
version "3.49.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029"
integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==
table@^6.0.9:
version "6.8.0"

View File

@@ -177,13 +177,11 @@ If you do not define the options, NextAuth.js will use the default values for th
#### wrap middleware
```ts title="middleware.ts"
import type { NextRequest } from "next/server"
import type { JWT } from "next-auth/jwt"
import { withAuth } from "next-auth/middleware"
export default withAuth(
// `withAuth` can augment your Request with the user's token.
function middleware(req: NextRequest & { nextauth: { token: JWT | null } }) {
// `withAuth` augments your `Request` with the user's token.
function middleware(req) {
console.log(req.nextauth.token)
},
{

View File

@@ -11,6 +11,7 @@ Without these people, the project could not have become one of the most used aut
- [Balázs Orbán](https://github.com/balazsorban44) - **Lead Maintainer**
- [Nico Domino](https://github.com/ndom91) - Maintainer (Documentation, Core)
- [Lluis Agusti](https://github.com/lluia) - Maintainer (Documentation, Testing, TypeScript)
- [Thang Huu Vu](https://github.com/ThangHuuVu) - Maintainer (Core, TypeScript)
## Special thanks

View File

@@ -61,17 +61,20 @@ There should also be further details logged when this occurs, such as the error
### Signin / Callback
#### GET_AUTHORIZATION_URL_ERROR
This error can occur when we cannot get the OAuth v1 request token and generate the authorization URL.
Please double check your OAuth v1 provider settings, especially the OAuth token and OAuth token secret.
#### SIGNIN_OAUTH_ERROR
This error can occur in one of a few places, first during the redirect to the authorization URL of the provider. Next, in the signin flow while creating the PKCE code verifier. Finally, during the generation of the CSRF Token hash in the internal state during signin.
This error occurs during the redirection to the authorization URL of the OAuth provider. Possible causes:
Please check your OAuth provider settings and make sure your URLs and other options are correctly set on the provider side.
1. Cookie handling
Either PKCE code verifier or the generation of the CSRF token hash in the internal state failed.
If set, check your [`cookies` configuration](/configuration/options#cookies), and make sure the browser is not blocking/restricting cookies.
2. OAuth misconfiguration
Please check your OAuth provider and make sure your URLs and other options are correctly set.
If you are using an OAuth v1 provider, check your OAuth v1 provider settings, especially the OAuth token and OAuth token secret.
#### CALLBACK_OAUTH_ERROR
@@ -151,12 +154,6 @@ This error occurs when there was an issue deleting the session from the database
### Other
#### SEND_VERIFICATION_EMAIL_ERROR
This error occurs when the Email Authentication Provider is unable to send an email.
Check your mail server configuration.
#### MISSING_NEXTAUTH_API_ROUTE_ERROR
This error happens when `[...nextauth].js` file is not found inside `pages/api/auth`.

View File

@@ -63,17 +63,32 @@ _If you use a custom credentials provider user accounts will not be persisted in
<details>
<summary>
<h3 style={{display:"inline-block"}}>Can I use NextAuth.js with a website that does not use Next.js?</h3>
<h3 style={{display:"inline-block"}}>Can I use NextAuth.js with a framework different than Next.js?</h3>
</summary>
<p>
NextAuth.js is designed for use with Next.js and Serverless.
NextAuth.js was originally designed for use with Next.js and Serverless. However, today you could use the NextAuth.js core with any other framework. Checkout the examples for <a href="https://github.com/nextauthjs/next-auth/tree/main/apps/example-gatsby" target="_blank">Gatsby</a> and <a href="https://github.com/nextauthjs/next-auth/tree/main/apps/playground-sveltekit" target="_blank">SvelteKit</a>. If you would add another integration with other frameworks, feel free to work on it and send a pull request. Make sure to check if there's any on-going work before open a new issue.
If you are using a different framework for your website, you can create a website that handles sign in with Next.js and then access those sessions on a website that does not use Next.js as long as the websites are on the same domain.
</p>
</details>
If you use NextAuth.js on a website with a different subdomain then the rest of your website (e.g. `auth.example.com` vs `www.example.com`) you will need to set a custom cookie domain policy for the Session Token cookie. (See also: [Cookies](/configuration/options#cookies))
<details>
<summary>
<h3 style={{display:"inline-block"}}>Can session generated by NextAuth.js be used by another website?</h3>
</summary>
<p>
NextAuth.js does not currently support automatically signing into sites on different top level domains (e.g. `www.example.com` vs `www.example.org`) using a single session.
**Same domain**: you can create a website that handles sign-in with NextAuth.js and then access those sessions on a website that does not use NextAuth.js as long as the websites are on the same domain.
**Same root domain, different subdomains**: If you use NextAuth.js on a website with a different subdomain than the rest of your website (e.g. `auth.example.com` vs. `www.example.com`) you will need to set a custom cookie domain policy for the Session Token cookie. (See also: [Cookies](/configuration/options#cookies)).
:::warning
Changing the default cookies domain policy is advanced and can lead to security issues if done correctly. Make sure you're aware of the security implication before proceeding.
:::
A working example can be found at <a href="https://github.com/vercel/examples/tree/main/solutions/subdomain-auth" target="_blank">this example repo</a>.
**Different root domains**: NextAuth.js does not currently support automatically signing into sites on different top-level domains (e.g. `www.example.com` vs. `www.example.org`) using a single session.
</p>
</details>

View File

@@ -148,13 +148,9 @@ Because of how `_app` is written, it won't unnecessarily contact the `/api/auth/
More information can be found in the following [GitHub Issue](https://github.com/nextauthjs/next-auth/issues/1210).
### NextAuth.js + React-Query
### NextAuth.js + React Query
There is also an alternative client-side API library based upon [`react-query`](https://www.npmjs.com/package/react-query) available under [`nextauthjs/react-query`](https://github.com/nextauthjs/react-query).
If you use `react-query` in your project already, you can leverage it with NextAuth.js to handle the client-side session management for you as well. This replaces NextAuth.js's native `useSession` and `SessionProvider` from `next-auth/react`.
See repository [`README`](https://github.com/nextauthjs/react-query) for more details.
You can create your own session management solution using data fetching libraries like [React Query](https://tanstack.com/query/v4/docs/adapters/react-query) or [SWR](https://swr.vercel.app). You can use the [original implementation of `@next-auth/react-query`](https://github.com/nextauthjs/react-query) and look at the [`next-auth/react` source code](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/react/index.tsx) as a starting point.
---
@@ -531,4 +527,4 @@ export default function App({
</SessionProvider>
)
}
```
```

View File

@@ -61,8 +61,9 @@ You can protect server side rendered pages using the `unstable_getServerSession`
You need to add this to every server rendered page you want to protect. Be aware, `unstable_getServerSession` takes slightly different arguments than the method it is replacing, `getSession`.
```js title="pages/server-side-example.js"
import { useSession, unstable_getServerSession } from "next-auth/next"
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "./api/auth/[...nextauth]"
import { useSession } from "next-auth/react"
export default function Page() {
const { data: session } = useSession()
@@ -144,10 +145,9 @@ If you are using JSON Web Tokens you can use the `getToken()` helper to access t
// This is an example of how to read a JSON Web Token from an API route
import { getToken } from "next-auth/jwt"
const secret = process.env.SECRET
export default async (req, res) => {
const token = await getToken({ req, secret })
// If you don't have NEXTAUTH_SECRET set, you will have to pass your secret as `secret` to `getToken`
const token = await getToken({ req })
if (token) {
// Signed in
console.log("JSON Web Token", JSON.stringify(token, null, 2))

View File

@@ -37,6 +37,12 @@ Twitter OAuth 2.0 is currently in beta as certain changes might still be necessa
Some APIs are still experimental; they may be changed or removed in the future. Use at your own risk.
#### DEBUG_ENABLED
You have enabled the `debug` option. It is meant for development only, to help you catch issues in your authentication flow and you should consider removing this option when deploying to production. One way of only allowing debugging while not in production is to set `debug: process.env.NODE_ENV !== "production"`, so you can commit this without needing to change the value.
If you want to log debug messages during production anyway, we recommend setting the [`logger` option](/configuration/options#logger) with proper sanitization of potentially sensitive user information.
## Adapter
### ADAPTER_TYPEORM_UPDATING_ENTITIES

View File

@@ -11,16 +11,16 @@
"test": "turbo run test --concurrency=1 --filter=!@next-auth/pouchdb-adapter --filter=!next-auth-* --filter=[HEAD^1]",
"setup": "turbo run setup",
"dev": "pnpm dev:app",
"email": "cd apps/dev && pnpm email",
"dev:app": "turbo run dev --parallel --no-deps --no-cache --filter=next-auth-app",
"dev:docs": "turbo run dev --parallel --no-deps --no-cache --filter=next-auth-docs",
"version:pr": "node ./config/version-pr",
"release": "ts-node scripts/release"
"release": "release"
},
"devDependencies": {
"@actions/core": "^1.6.0",
"@commitlint/parse": "16.0.0",
"@balazsorban/monorepo-release": "0.0.4",
"@types/node": "^17.0.25",
"@types/semver": "7.3.9",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.32.0",
@@ -30,15 +30,10 @@
"eslint-plugin-jest": "^25.3.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"git-log-parser": "1.2.0",
"husky": "^7.0.4",
"prettier": "2.4.1",
"pretty-quick": "^3.1.2",
"semver": "7.3.5",
"stream-to-array": "2.3.0",
"ts-node": "10.8.2",
"turbo": "1.3.1",
"type-fest": "2.16.0",
"typescript": "^4.5.2"
},
"engines": {
@@ -47,7 +42,15 @@
},
"prettier": {
"semi": false,
"singleQuote": false
"singleQuote": false,
"overrides": [
{
"files": "apps/dev/pages/api/auth/[...nextauth].ts",
"options": {
"printWidth": 150
}
}
]
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/dgraph-adapter",
"version": "1.0.3",
"version": "1.0.4",
"description": "Dgraph adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -31,7 +31,7 @@
},
"peerDependencies": {
"jsonwebtoken": "^8.5.1",
"next-auth": "workspace:*"
"next-auth": "^4"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:^0.0.0",
@@ -50,4 +50,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@next-auth/dynamodb-adapter",
"repository": "https://github.com/nextauthjs/next-auth",
"version": "1.0.3",
"version": "1.0.4",
"description": "AWS DynamoDB adapter for next-auth.",
"keywords": [
"next-auth",
@@ -32,7 +32,7 @@
"license": "ISC",
"peerDependencies": {
"@aws-sdk/lib-dynamodb": "^3.36.1",
"next-auth": "workspace:*"
"next-auth": "^4"
},
"devDependencies": {
"@aws-sdk/client-dynamodb": "^3.36.1",
@@ -43,4 +43,4 @@
"jest": "^27.4.3",
"next-auth": "workspace:*"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/fauna-adapter",
"version": "1.0.3",
"version": "1.0.4",
"description": "Fauna Adapter for NextAuth",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -41,7 +41,7 @@
},
"peerDependencies": {
"faunadb": "^4.3.0",
"next-auth": "workspace:*"
"next-auth": "^4"
},
"devDependencies": {
"@fauna-labs/fauna-schema-migrate": "^2.1.3",
@@ -54,4 +54,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/firebase-adapter",
"version": "1.0.0",
"version": "1.0.1",
"description": "Firebase adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -33,7 +33,7 @@
},
"peerDependencies": {
"firebase": "^9.7.0",
"next-auth": "workspace:*"
"next-auth": "^4"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:^0.0.0",
@@ -43,4 +43,4 @@
"jest": "^27.4.3",
"next-auth": "workspace:*"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/mongodb-adapter",
"version": "1.0.3",
"version": "1.0.4",
"description": "mongoDB adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -32,7 +32,7 @@
],
"peerDependencies": {
"mongodb": "^4.1.1",
"next-auth": "workspace:*"
"next-auth": "^4"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:^0.0.0",
@@ -44,4 +44,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/neo4j-adapter",
"version": "1.0.3",
"version": "1.0.4",
"description": "neo4j adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -34,7 +34,7 @@
],
"peerDependencies": {
"neo4j-driver": "^4.0.0",
"next-auth": "workspace:*"
"next-auth": "^4"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:^0.0.0",
@@ -50,4 +50,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/pouchdb-adapter",
"version": "0.1.3",
"version": "0.1.4",
"description": "PouchDB adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -30,7 +30,7 @@
"dist"
],
"peerDependencies": {
"next-auth": "workspace:*",
"next-auth": "^3",
"pouchdb": "^7.2.2",
"pouchdb-find": "^7.2.2"
},
@@ -51,4 +51,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/prisma-adapter",
"version": "1.0.3",
"version": "1.0.4",
"description": "Prisma adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -37,7 +37,7 @@
],
"peerDependencies": {
"@prisma/client": ">=2.26.0 || >=3",
"next-auth": "workspace:*"
"next-auth": "^4"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:^0.0.0",
@@ -51,4 +51,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/upstash-redis-adapter",
"version": "3.0.0",
"version": "3.0.1",
"description": "Upstash adapter for next-auth. It uses Upstash's connectionless (HTTP based) Redis client.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -31,7 +31,7 @@
],
"peerDependencies": {
"@upstash/redis": "^1.0.1",
"next-auth": "workspace:*"
"next-auth": "^4"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:^0.0.0",
@@ -39,6 +39,7 @@
"@types/uuid": "^8.3.3",
"@upstash/redis": "^1.0.1",
"dotenv": "^10.0.0",
"isomorphic-fetch": "3.0.0",
"jest": "^27.4.3",
"next-auth": "workspace:*"
},
@@ -48,4 +49,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -1,3 +1,4 @@
import "isomorphic-fetch"
import { Redis } from "@upstash/redis"
import { runBasicTests } from "@next-auth/adapter-test"
import { hydrateDates, UpstashRedisAdapter } from "../src"
@@ -7,41 +8,42 @@ if (!process.env.UPSTASH_REDIS_URL || !process.env.UPSTASH_REDIS_KEY) {
test("Skipping UpstashRedisAdapter tests, since required environment variables aren't available", () => {
expect(true).toBe(true)
})
} else {
const client = new Redis({
url: process.env.UPSTASH_REDIS_URL,
token: process.env.UPSTASH_REDIS_KEY,
})
runBasicTests({
adapter: UpstashRedisAdapter(client, { baseKeyPrefix: "testApp:" }),
db: {
async user(id: string) {
const data = await client.get<object>(`testApp:user:${id}`)
if (!data) return null
return hydrateDates(data)
},
async account({ provider, providerAccountId }) {
const data = await client.get<object>(
`testApp:user:account:${provider}:${providerAccountId}`
)
if (!data) return null
return hydrateDates(data)
},
async session(sessionToken) {
const data = await client.get<object>(
`testApp:user:session:${sessionToken}`
)
if (!data) return null
return hydrateDates(data)
},
async verificationToken(where) {
const data = await client.get<object>(
`testApp:user:token:${where.identifier}`
)
if (!data) return null
return hydrateDates(data)
},
},
})
process.exit(0)
}
const client = new Redis({
url: process.env.UPSTASH_REDIS_URL,
token: process.env.UPSTASH_REDIS_KEY,
})
runBasicTests({
adapter: UpstashRedisAdapter(client, { baseKeyPrefix: "testApp:" }),
db: {
async user(id: string) {
const data = await client.get<object>(`testApp:user:${id}`)
if (!data) return null
return hydrateDates(data)
},
async account({ provider, providerAccountId }) {
const data = await client.get<object>(
`testApp:user:account:${provider}:${providerAccountId}`
)
if (!data) return null
return hydrateDates(data)
},
async session(sessionToken) {
const data = await client.get<object>(
`testApp:user:session:${sessionToken}`
)
if (!data) return null
return hydrateDates(data)
},
async verificationToken(where) {
const data = await client.get<object>(
`testApp:user:token:${where.identifier}`
)
if (!data) return null
return hydrateDates(data)
},
},
})

View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "4.10.0",
"version": "4.10.2",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
@@ -8,7 +8,8 @@
"contributors": [
"Balázs Orbán <info@balazsorban.com>",
"Nico Domino <yo@ndo.dev>",
"Lluis Agusti <hi@llu.lu>"
"Lluis Agusti <hi@llu.lu>",
"Thang Huu Vu <thvu@hey.com>"
],
"main": "index.js",
"module": "index.js",
@@ -138,4 +139,4 @@
"**/tests",
"**/__tests__"
]
}
}

View File

@@ -79,7 +79,7 @@ test("when the fetch fails it'll throw a client fetch error", async () => {
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
path: "csrf",
url: "/api/auth/csrf",
error: new SyntaxError("Unexpected token s in JSON at position 0"),
})
})

View File

@@ -57,7 +57,7 @@ test("when failing to fetch the providers, it'll log the error", async () => {
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
path: "providers",
url: "/api/auth/providers",
error: new SyntaxError("Unexpected token s in JSON at position 0"),
})
})

View File

@@ -71,7 +71,7 @@ test("if there's an error fetching the session, it should log it", async () => {
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
path: "session",
url: "/api/auth/session",
error: new SyntaxError("Unexpected token S in JSON at position 0"),
})
})

View File

@@ -256,7 +256,7 @@ test("when it fails to fetch the providers, it redirected back to signin page",
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
error: "Error when retrieving providers",
path: "providers",
url: "/api/auth/providers",
})
})
})

View File

@@ -35,20 +35,17 @@ export async function fetchData<T = any>(
logger: LoggerInstance,
{ ctx, req = ctx?.req }: CtxOrReq = {}
): Promise<T | null> {
const url = `${apiBaseUrl(__NEXTAUTH)}/${path}`
try {
const options = req?.headers.cookie
? { headers: { cookie: req.headers.cookie } }
: {}
const res = await fetch(`${apiBaseUrl(__NEXTAUTH)}/${path}`, options)
const res = await fetch(url, options)
const data = await res.json()
if (!res.ok) throw data
return Object.keys(data).length > 0 ? data : null // Return null if data empty
} catch (error) {
logger.error("CLIENT_FETCH_ERROR", {
error: error as Error,
path,
...(req ? { header: req.headers } : {}),
})
logger.error("CLIENT_FETCH_ERROR", { error: error as Error, url })
return null
}
}

View File

@@ -90,8 +90,8 @@ export async function NextAuthHandler<
const assertionResult = assertConfig({ options: userOptions, req })
if (typeof assertionResult === "string") {
logger.warn(assertionResult)
if (Array.isArray(assertionResult)) {
assertionResult.forEach(logger.warn)
} else if (assertionResult instanceof Error) {
// Bail out early if there's an error in the user config
const { pages, theme } = userOptions

View File

@@ -3,7 +3,7 @@ import logger from "../utils/logger"
import parseUrl from "../utils/parse-url"
import { adapterErrorHandler, eventsErrorHandler } from "./errors"
import parseProviders from "./lib/providers"
import createSecret from "./lib/utils"
import { createSecret } from "./lib/utils"
import * as cookie from "./lib/cookie"
import * as jwt from "../jwt"
import { defaultCallbacks } from "./lib/default-callbacks"

View File

@@ -9,8 +9,9 @@ import {
import parseUrl from "../../utils/parse-url"
import { defaultCookies } from "./cookie"
import type { NextAuthHandlerParams, RequestInternal } from ".."
import type { RequestInternal } from ".."
import type { WarningCode } from "../../utils/logger"
import type { NextAuthOptions } from "../types"
type ConfigError =
| MissingAPIRoute
@@ -19,7 +20,7 @@ type ConfigError =
| MissingAuthorize
| MissingAdapter
let twitterWarned = false
let warned = false
function isValidHttpUrl(url: string, baseUrl: string) {
try {
@@ -37,13 +38,28 @@ function isValidHttpUrl(url: string, baseUrl: string) {
*
* REVIEW: Make some of these and corresponding docs less Next.js specific?
*/
export function assertConfig(
params: NextAuthHandlerParams & {
req: RequestInternal
}
): ConfigError | WarningCode | undefined {
export function assertConfig(params: {
options: NextAuthOptions
req: RequestInternal
}): ConfigError | WarningCode[] {
const { options, req } = params
const warnings: WarningCode[] = []
if (!warned) {
if (!req.host) warnings.push("NEXTAUTH_URL")
// TODO: Make this throw an error in next major. This will also get rid of `NODE_ENV`
if (!options.secret && process.env.NODE_ENV !== "production")
warnings.push("NO_SECRET")
if (options.debug) warnings.push("DEBUG_ENABLED")
}
if (!options.secret && process.env.NODE_ENV === "production") {
return new MissingSecret("Please define a `secret` in production.")
}
// req.query isn't defined when asserting `unstable_getServerSession` for example
if (!req.query?.nextauth && !req.action) {
return new MissingAPIRoute(
@@ -51,14 +67,6 @@ export function assertConfig(
)
}
if (!options.secret) {
if (process.env.NODE_ENV === "production") {
return new MissingSecret("Please define a `secret` in production.")
} else {
return "NO_SECRET"
}
}
const callbackUrlParam = req.query?.callbackUrl as string | undefined
const url = parseUrl(req.host)
@@ -69,9 +77,6 @@ export function assertConfig(
)
}
// This is below the callbackUrlParam check because it would obscure the error
if (!req.host) return "NEXTAUTH_URL"
const { callbackUrl: defaultCallbackUrl } = defaultCookies(
options.useSecureCookies ?? url.base.startsWith("https://")
)
@@ -119,8 +124,10 @@ export function assertConfig(
return new MissingAdapter("E-mail login requires an adapter.")
}
if (!twitterWarned && hasTwitterOAuth2) {
twitterWarned = true
return "TWITTER_OAUTH_2_BETA"
if (!warned) {
if (hasTwitterOAuth2) warnings.push("TWITTER_OAUTH_2_BETA")
warned = true
}
return warnings
}

View File

@@ -9,9 +9,8 @@ import type { InternalOptions } from "../../types"
export default async function email(
identifier: string,
options: InternalOptions<"email">
) {
const { url, adapter, provider, logger, callbackUrl, theme } = options
): Promise<string> {
const { url, adapter, provider, callbackUrl, theme } = options
// Generate token
const token =
(await provider.generateVerificationToken?.()) ??
@@ -34,22 +33,18 @@ export default async function email(
const params = new URLSearchParams({ callbackUrl, token, email: identifier })
const _url = `${url}/callback/${provider.id}?${params}`
try {
// Send to user
await provider.sendVerificationRequest({
identifier,
token,
expires,
url: _url,
provider,
theme,
})
} catch (error) {
logger.error("SEND_VERIFICATION_EMAIL_ERROR", {
identifier,
url,
error: error as Error,
})
throw new Error("SEND_VERIFICATION_EMAIL_ERROR")
}
// Send to user
await provider.sendVerificationRequest({
identifier,
token,
expires,
url: _url,
provider,
theme,
})
return `${url}/verify-request?${new URLSearchParams({
provider: provider.id,
type: provider.type,
})}`
}

View File

@@ -14,66 +14,63 @@ import type { Cookie } from "../cookie"
*
* [OAuth 2](https://www.oauth.com/oauth2-servers/authorization/the-authorization-request/) | [OAuth 1](https://oauth.net/core/1.0a/#auth_step2)
*/
export default async function getAuthorizationUrl(params: {
export default async function getAuthorizationUrl({
options,
query,
}: {
options: InternalOptions<"oauth">
query: RequestInternal["query"]
}) {
const { options, query } = params
const { logger, provider } = options
try {
let params: any = {}
let params: any = {}
if (typeof provider.authorization === "string") {
const parsedUrl = new URL(provider.authorization)
const parsedParams = Object.fromEntries(parsedUrl.searchParams.entries())
params = { ...params, ...parsedParams }
} else {
params = { ...params, ...provider.authorization?.params }
}
params = { ...params, ...query }
// Handle OAuth v1.x
if (provider.version?.startsWith("1.")) {
const client = oAuth1Client(options)
const tokens = (await client.getOAuthRequestToken(params)) as any
const url = `${
// @ts-expect-error
provider.authorization?.url ?? provider.authorization
}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params,
})}`
logger.debug("GET_AUTHORIZATION_URL", { url })
return { redirect: url }
}
const client = await openidClient(options)
const authorizationParams: AuthorizationParameters = params
const cookies: Cookie[] = []
const state = await createState(options)
if (state) {
authorizationParams.state = state.value
cookies.push(state.cookie)
}
const pkce = await createPKCE(options)
if (pkce) {
authorizationParams.code_challenge = pkce.code_challenge
authorizationParams.code_challenge_method = pkce.code_challenge_method
cookies.push(pkce.cookie)
}
const url = client.authorizationUrl(authorizationParams)
logger.debug("GET_AUTHORIZATION_URL", { url, cookies })
return { redirect: url, cookies }
} catch (error) {
logger.error("GET_AUTHORIZATION_URL_ERROR", error as Error)
throw error
if (typeof provider.authorization === "string") {
const parsedUrl = new URL(provider.authorization)
const parsedParams = Object.fromEntries(parsedUrl.searchParams.entries())
params = { ...params, ...parsedParams }
} else {
params = { ...params, ...provider.authorization?.params }
}
params = { ...params, ...query }
// Handle OAuth v1.x
if (provider.version?.startsWith("1.")) {
const client = oAuth1Client(options)
const tokens = (await client.getOAuthRequestToken(params)) as any
const url = `${
// @ts-expect-error
provider.authorization?.url ?? provider.authorization
}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params,
})}`
logger.debug("GET_AUTHORIZATION_URL", { url, provider })
return { redirect: url }
}
const client = await openidClient(options)
const authorizationParams: AuthorizationParameters = params
const cookies: Cookie[] = []
const state = await createState(options)
if (state) {
authorizationParams.state = state.value
cookies.push(state.cookie)
}
const pkce = await createPKCE(options)
if (pkce) {
authorizationParams.code_challenge = pkce.code_challenge
authorizationParams.code_challenge_method = pkce.code_challenge_method
cookies.push(pkce.cookie)
}
const url = client.authorizationUrl(authorizationParams)
logger.debug("GET_AUTHORIZATION_URL", { url, cookies, provider })
return { redirect: url, cookies }
}

View File

@@ -28,9 +28,9 @@ export default async function oAuthCallback(params: {
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", {
error,
error_description: query?.error_description,
body,
providerId: provider.id,
})
logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", { body })
throw error
}
@@ -199,10 +199,7 @@ async function getProfile({
// all providers, so we return an empty object; the user should then be
// redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider.
logger.error("OAUTH_PARSE_PROFILE_ERROR", {
error: error as Error,
OAuthProfile,
})
logger.error("OAUTH_PARSE_PROFILE_ERROR", error as Error)
return {
profile: null,
account: null,

View File

@@ -28,7 +28,7 @@ export function hashToken(token: string, options: InternalOptions<"email">) {
* If no secret option is specified then it creates one on the fly
* based on options passed here. If options contains unique data, such as
* OAuth provider secrets and database credentials it should be sufficent. If no secret provided in production, we throw an error. */
export default function createSecret(params: {
export function createSecret(params: {
userOptions: NextAuthOptions
url: InternalUrl
}) {
@@ -36,6 +36,7 @@ export default function createSecret(params: {
return (
userOptions.secret ??
// TODO: Remove falling back to default secret, and error in dev if one isn't provided
createHash("sha256")
.update(JSON.stringify({ ...url, ...userOptions }))
.digest("hex")

View File

@@ -26,7 +26,10 @@ export default async function signin(params: {
const response = await getAuthorizationUrl({ options, query })
return response
} catch (error) {
logger.error("SIGNIN_OAUTH_ERROR", { error: error as Error, provider })
logger.error("SIGNIN_OAUTH_ERROR", {
error: error as Error,
providerId: provider.id,
})
return { redirect: `${url}/error?error=OAuthSignin` }
}
} else if (provider.type === "email") {
@@ -79,18 +82,15 @@ export default async function signin(params: {
}
try {
await emailSignin(email, options)
const redirect = await emailSignin(email, options)
return { redirect }
} catch (error) {
logger.error("SIGNIN_EMAIL_ERROR", error as Error)
logger.error("SIGNIN_EMAIL_ERROR", {
error: error as Error,
providerId: provider.id,
})
return { redirect: `${url}/error?error=EmailSignin` }
}
const params = new URLSearchParams({
provider: provider.id,
type: provider.type,
})
return { redirect: `${url}/verify-request?${params}` }
}
return { redirect: `${url}/signin` }
}

View File

@@ -92,10 +92,14 @@ export interface NextAuthMiddlewareOptions {
secret?: string
}
// TODO: `NextMiddleware` should allow returning `void`
// Simplify when https://github.com/vercel/next.js/pull/38625 is merged.
type NextMiddlewareResult = ReturnType<NextMiddleware> | void // eslint-disable-line @typescript-eslint/no-invalid-void-type
async function handleMiddleware(
req: NextRequest,
options: NextAuthMiddlewareOptions | undefined,
onSuccess?: (token: JWT | null) => Promise<any>
onSuccess?: (token: JWT | null) => Promise<NextMiddlewareResult>
) {
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
const errorPage = options?.pages?.error ?? "/api/auth/error"
@@ -143,12 +147,21 @@ async function handleMiddleware(
return NextResponse.redirect(signInUrl)
}
export interface NextRequestWithAuth extends NextRequest {
nextauth: { token: JWT | null }
}
export type NextMiddlewareWithAuth = (
request: NextRequestWithAuth,
event: NextFetchEvent
) => NextMiddlewareResult | Promise<NextMiddlewareResult>
export type WithAuthArgs =
| [NextRequest]
| [NextRequest, NextFetchEvent]
| [NextRequest, NextAuthMiddlewareOptions]
| [NextMiddleware]
| [NextMiddleware, NextAuthMiddlewareOptions]
| [NextRequestWithAuth]
| [NextRequestWithAuth, NextFetchEvent]
| [NextRequestWithAuth, NextAuthMiddlewareOptions]
| [NextMiddlewareWithAuth]
| [NextMiddlewareWithAuth, NextAuthMiddlewareOptions]
| [NextAuthMiddlewareOptions]
| []
@@ -176,9 +189,9 @@ export function withAuth(...args: WithAuthArgs) {
if (typeof args[0] === "function") {
const middleware = args[0]
const options = args[1] as NextAuthMiddlewareOptions | undefined
return async (...args: Parameters<NextMiddleware>) =>
return async (...args: Parameters<NextMiddlewareWithAuth>) =>
await handleMiddleware(args[0], options, async (token) => {
;(args[0] as any).nextauth = { token }
args[0].nextauth = { token }
return await middleware(...args)
})
}

View File

@@ -19,16 +19,19 @@ export interface AzureB2CProfile extends Record<string, any> {
export default function AzureADB2C<P extends AzureB2CProfile>(
options: OAuthUserConfig<P> & {
primaryUserFlow: string
tenantId: string
primaryUserFlow?: string
tenantId?: string
}
): OAuthConfig<P> {
const { tenantId, primaryUserFlow } = options
const issuer =
options.issuer ??
`https://${tenantId}.b2clogin.com/${tenantId}.onmicrosoft.com/${primaryUserFlow}/v2.0`
return {
id: "azure-ad-b2c",
name: "Azure Active Directory B2C",
type: "oauth",
wellKnown: `https://${tenantId}.b2clogin.com/${tenantId}.onmicrosoft.com/${primaryUserFlow}/v2.0/.well-known/openid-configuration`,
wellKnown: `${issuer}/.well-known/openid-configuration`,
idToken: true,
profile(profile) {
return {

View File

@@ -1,8 +1,6 @@
import type { OAuthConfig, OAuthUserConfig } from "."
/**
* Source https://docs.github.com/en/rest/users/users#get-the-authenticated-user
*/
/** @see https://docs.github.com/en/rest/users/users#get-the-authenticated-user */
export interface GithubProfile extends Record<string, any> {
login: string
id: number
@@ -55,7 +53,7 @@ export interface GithubEmail extends Record<string, any> {
email: string
primary: boolean
verified: boolean
visibility: string | null
visibility: "public" | "private"
}
export default function Github<P extends GithubProfile>(
@@ -67,29 +65,25 @@ export default function Github<P extends GithubProfile>(
type: "oauth",
authorization: {
url: "https://github.com/login/oauth/authorize",
params: { scope: "read:user+user:email" },
params: { scope: "read:user user:email" },
},
token: "https://github.com/login/oauth/access_token",
userinfo: {
url: "https://api.github.com/user",
async request({ client, tokens }) {
// Get base profile
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const profile = await client.userinfo(tokens.access_token!)
// If user has email hidden, get their primary email from the GitHub API
if (!profile.email) {
const emails: GithubEmail[] = await (
await fetch("https://api.github.com/user/emails", {
headers: { Authorization: `token ${tokens.access_token}` },
})
).json()
// If the user does not have a public email, get another via the GitHub API
// See https://docs.github.com/en/rest/users/emails#list-public-email-addresses-for-the-authenticated-user
const res = await fetch("https://api.github.com/user/emails", {
headers: { Authorization: `token ${tokens.access_token}` },
})
if (emails?.length > 0) {
// Get primary email
profile.email = emails.find((email) => email.primary)?.email
// And if for some reason it doesn't exist, just use the first
if (!profile.email) profile.email = emails[0].email
if (res.ok) {
const emails: GithubEmail[] = await res.json()
profile.email = (emails.find((e) => e.primary) ?? emails[0]).email
}
}

View File

@@ -1,22 +0,0 @@
/** @type {import("src/providers").OAuthProvider} */
/** @type {import(".").OAuthProvider} */
export default function GitLab(options) {
return {
id: "gitlab",
name: "GitLab",
type: "oauth",
authorization: "https://gitlab.com/oauth/authorize?scope=read_user",
token: "https://gitlab.com/oauth/token",
userinfo: "https://gitlab.com/api/v4/user",
checks: ["pkce", "state"],
profile(profile) {
return {
id: profile.id,
name: profile.username,
email: profile.email,
image: profile.avatar_url,
}
},
options,
}
}

View File

@@ -0,0 +1,72 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface GitLabProfile extends Record<string, any> {
id: number
username: string
email: string
name: string
state: string
avatar_url: string
web_url: string
created_at: string
bio: string
location?: string
public_email: string
skype: string
linkedin: string
twitter: string
website_url: string
organization: string
job_title: string
pronouns: string
bot: boolean
work_information?: string
followers: number
following: number
local_time: string
last_sign_in_at: string
confirmed_at: string
theme_id: number
last_activity_on: string
color_scheme_id: number
projects_limit: number
current_sign_in_at: string
identities: Array<{
provider: string
extern_uid: string
}>
can_create_group: boolean
can_create_project: boolean
two_factor_enabled: boolean
external: boolean
private_profile: boolean
commit_email: string
shared_runners_minutes_limit: number
extra_shared_runners_minutes_limit: number
}
export default function GitLab<P extends GitLabProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "gitlab",
name: "GitLab",
type: "oauth",
authorization: {
url: "https://gitlab.com/oauth/authorize",
params: { scope: "read_user" },
},
token: "https://gitlab.com/oauth/token",
userinfo: "https://gitlab.com/api/v4/user",
checks: ["pkce", "state"],
profile(profile) {
return {
id: profile.id.toString(),
name: profile.name ?? profile.username,
email: profile.email,
image: profile.avatar_url,
}
},
options,
}
}

View File

@@ -175,7 +175,11 @@ export async function getProviders() {
export async function signIn<
P extends RedirectableProviderType | undefined = undefined
>(
provider?: LiteralUnion<P extends RedirectableProviderType ? P | BuiltInProviderType : BuiltInProviderType>,
provider?: LiteralUnion<
P extends RedirectableProviderType
? P | BuiltInProviderType
: BuiltInProviderType
>,
options?: SignInOptions,
authorizationParams?: SignInAuthorizationParams
): Promise<
@@ -224,6 +228,7 @@ export async function signIn<
const data = await res.json()
// TODO: Do not redirect for Credentials and Email providers by default in next major
if (redirect || !isSupportingReturn) {
const url = data.url ?? callbackUrl
window.location.href = url

View File

@@ -19,7 +19,11 @@ function hasErrorProperty(
return !!(x as any)?.error
}
export type WarningCode = "NEXTAUTH_URL" | "NO_SECRET" | "TWITTER_OAUTH_2_BETA"
export type WarningCode =
| "NEXTAUTH_URL"
| "NO_SECRET"
| "TWITTER_OAUTH_2_BETA"
| "DEBUG_ENABLED"
/**
* Override any of the methods, and the rest will use the default logger.

View File

@@ -15,7 +15,9 @@
"skipDefaultLibCheck": true,
"baseUrl": ".",
"outDir": ".",
"paths": {
"next": ["node_modules/next"]
}
},
"exclude": ["./*.js", "./*.d.ts", "config", "**/__tests__", "tests"]
}

View File

@@ -1,5 +1,6 @@
{
"name": "@next-auth/tsconfig",
"private": true,
"version": "0.0.0",
"license": "MIT",
"main": "index.js",
@@ -7,4 +8,4 @@
"adapters.json",
"base.json"
]
}
}

135
pnpm-lock.yaml generated
View File

@@ -5,9 +5,8 @@ importers:
.:
specifiers:
'@actions/core': ^1.6.0
'@commitlint/parse': 16.0.0
'@balazsorban/monorepo-release': 0.0.4
'@types/node': ^17.0.25
'@types/semver': 7.3.9
'@typescript-eslint/eslint-plugin': ^5.10.2
'@typescript-eslint/parser': ^4.33.0
eslint: ^7.32.0
@@ -17,21 +16,15 @@ importers:
eslint-plugin-jest: ^25.3.0
eslint-plugin-node: ^11.1.0
eslint-plugin-promise: ^6.0.0
git-log-parser: 1.2.0
husky: ^7.0.4
prettier: 2.4.1
pretty-quick: ^3.1.2
semver: 7.3.5
stream-to-array: 2.3.0
ts-node: 10.8.2
turbo: 1.3.1
type-fest: 2.16.0
typescript: ^4.5.2
devDependencies:
'@actions/core': 1.9.0
'@commitlint/parse': 16.0.0
'@balazsorban/monorepo-release': 0.0.4
'@types/node': 17.0.45
'@types/semver': 7.3.9
'@typescript-eslint/eslint-plugin': 5.29.0_3ekaj7j3owlolnuhj3ykrb7u7i
'@typescript-eslint/parser': 4.33.0_hxadhbs2xogijvk7vq4t2azzbu
eslint: 7.32.0
@@ -41,15 +34,10 @@ importers:
eslint-plugin-jest: 25.7.0_vibe533nrfhlkvcegtsn4treva
eslint-plugin-node: 11.1.0_eslint@7.32.0
eslint-plugin-promise: 6.0.0_eslint@7.32.0
git-log-parser: 1.2.0
husky: 7.0.4
prettier: 2.4.1
pretty-quick: 3.1.3_prettier@2.4.1
semver: 7.3.5
stream-to-array: 2.3.0
ts-node: 10.8.2_x2utdhayajzrh747hktprshhby
turbo: 1.3.1
type-fest: 2.16.0
typescript: 4.7.4
apps/dev:
@@ -389,6 +377,7 @@ importers:
'@types/uuid': ^8.3.3
'@upstash/redis': ^1.0.1
dotenv: ^10.0.0
isomorphic-fetch: 3.0.0
jest: ^27.4.3
next-auth: workspace:*
uuid: ^8.3.2
@@ -400,6 +389,7 @@ importers:
'@types/uuid': 8.3.4
'@upstash/redis': 1.7.0
dotenv: 10.0.0
isomorphic-fetch: 3.0.0
jest: 27.5.1
next-auth: link:../next-auth
@@ -3640,6 +3630,17 @@ packages:
'@babel/helper-validator-identifier': 7.16.7
to-fast-properties: 2.0.0
/@balazsorban/monorepo-release/0.0.4:
resolution: {integrity: sha512-jjYc05vcRueT+nC7BD7C0D2JjE+H8xDdAIfwjtlbMHTnTwPx2KYXrbWohbL7bGVN8ZbhJDmXkXOQjppSrZCQBw==}
engines: {node: '>=16.16.0'}
hasBin: true
dependencies:
'@commitlint/parse': 17.0.0
git-log-parser: 1.2.0
semver: 7.3.7
stream-to-array: 2.3.0
dev: true
/@bcoe/v8-coverage/0.2.3:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
dev: true
@@ -3653,29 +3654,22 @@ packages:
engines: {node: '>=0.1.90'}
requiresBuild: true
/@commitlint/parse/16.0.0:
resolution: {integrity: sha512-F9EjFlMw4MYgBEqoRrWZZKQBzdiJzPBI0qFDFqwUvfQsMmXEREZ242T4R5bFwLINWaALFLHEIa/FXEPa6QxCag==}
engines: {node: '>=v12'}
/@commitlint/parse/17.0.0:
resolution: {integrity: sha512-cKcpfTIQYDG1ywTIr5AG0RAiLBr1gudqEsmAGCTtj8ffDChbBRxm6xXs2nv7GvmJN7msOt7vOKleLvcMmRa1+A==}
engines: {node: '>=v14'}
dependencies:
'@commitlint/types': 16.2.1
'@commitlint/types': 17.0.0
conventional-changelog-angular: 5.0.13
conventional-commits-parser: 3.2.4
dev: true
/@commitlint/types/16.2.1:
resolution: {integrity: sha512-7/z7pA7BM0i8XvMSBynO7xsB3mVQPUZbVn6zMIlp/a091XJ3qAXRXc+HwLYhiIdzzS5fuxxNIHZMGHVD4HJxdA==}
engines: {node: '>=v12'}
/@commitlint/types/17.0.0:
resolution: {integrity: sha512-hBAw6U+SkAT5h47zDMeOu3HSiD0SODw4Aq7rRNh1ceUmL7GyLKYhPbUvlRWqZ65XjBLPHZhFyQlRaPNz8qvUyQ==}
engines: {node: '>=v14'}
dependencies:
chalk: 4.1.2
dev: true
/@cspotcode/source-map-support/0.8.1:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
dependencies:
'@jridgewell/trace-mapping': 0.3.9
dev: true
/@dabh/diagnostics/2.0.3:
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
dependencies:
@@ -5499,13 +5493,6 @@ packages:
'@jridgewell/resolve-uri': 3.0.7
'@jridgewell/sourcemap-codec': 1.4.13
/@jridgewell/trace-mapping/0.3.9:
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
dependencies:
'@jridgewell/resolve-uri': 3.0.7
'@jridgewell/sourcemap-codec': 1.4.13
dev: true
/@js-joda/core/3.2.0:
resolution: {integrity: sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==}
dev: true
@@ -6442,22 +6429,6 @@ packages:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
/@tsconfig/node10/1.0.9:
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
dev: true
/@tsconfig/node12/1.0.11:
resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
dev: true
/@tsconfig/node14/1.0.3:
resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
dev: true
/@tsconfig/node16/1.0.3:
resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==}
dev: true
/@types/aria-query/4.2.2:
resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==}
dev: true
@@ -6954,10 +6925,6 @@ packages:
/@types/scheduler/0.16.2:
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
/@types/semver/7.3.9:
resolution: {integrity: sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==}
dev: true
/@types/serve-index/1.9.1:
resolution: {integrity: sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==}
dependencies:
@@ -7764,10 +7731,6 @@ packages:
dev: true
optional: true
/arg/4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
dev: true
/arg/5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
dev: false
@@ -9435,10 +9398,6 @@ packages:
readable-stream: 3.6.0
dev: true
/create-require/1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
dev: true
/cross-env/5.2.1:
resolution: {integrity: sha512-1yHhtcfAd1r4nwQgknowuUNfIT9E8dOMMspC36g45dN+iD1blloi7xp8X/xAIDnjHWyt1uQ8PHk2fkNaym7soQ==}
engines: {node: '>=4.0'}
@@ -10568,11 +10527,6 @@ packages:
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
dev: true
/diff/4.0.2:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
dev: true
/dir-glob/3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
@@ -20934,37 +20888,6 @@ packages:
yargs-parser: 20.2.9
dev: true
/ts-node/10.8.2_x2utdhayajzrh747hktprshhby:
resolution: {integrity: sha512-LYdGnoGddf1D6v8REPtIH+5iq/gTDuZqv2/UJUU7tKjuEU8xVZorBM+buCGNjj+pGEud+sOoM4CX3/YzINpENA==}
hasBin: true
peerDependencies:
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
'@types/node': '*'
typescript: '>=2.7'
peerDependenciesMeta:
'@swc/core':
optional: true
'@swc/wasm':
optional: true
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.9
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.3
'@types/node': 17.0.45
acorn: 8.7.1
acorn-walk: 8.2.0
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 4.7.4
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
dev: true
/tsconfig-paths/3.14.1:
resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==}
dependencies:
@@ -21204,11 +21127,6 @@ packages:
engines: {node: '>=12.20'}
dev: false
/type-fest/2.16.0:
resolution: {integrity: sha512-qpaThT2HQkFb83gMOrdKVsfCN7LKxP26Yq+smPzY1FqoHRjqmjqHXA7n5Gkxi8efirtbeEUxzfEdePthQWCuHw==}
engines: {node: '>=12.20'}
dev: true
/type-is/1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@@ -21759,10 +21677,6 @@ packages:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
/v8-compile-cache-lib/3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
dev: true
/v8-compile-cache/2.3.0:
resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
dev: true
@@ -22422,11 +22336,6 @@ packages:
yargs-parser: 21.0.1
dev: true
/yn/3.1.1:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'}
dev: true
/yocto-queue/0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}

View File

@@ -1,168 +0,0 @@
import type {
Commit,
GroupedCommits as GrouppedCommits,
PackageToRelease,
} from "./types"
import { debug, pkgJson, execSync } from "./utils"
import semver from "semver"
import parseCommit from "@commitlint/parse"
import gitLog from "git-log-parser"
import streamToArray from "stream-to-array"
export async function analyze(options: {
dryRun: boolean
packages: Record<string, string>
BREAKING_COMMIT_MSG: string
RELEASE_COMMIT_MSG: string
RELEASE_COMMIT_TYPES: string[]
}): Promise<PackageToRelease[]> {
const {
packages,
BREAKING_COMMIT_MSG,
RELEASE_COMMIT_MSG,
RELEASE_COMMIT_TYPES,
} = options
const packageFolders = Object.values(options.packages)
console.log("Identifying latest tag...")
const latestTag = execSync("git describe --tags --abbrev=0", {
stdio: "pipe",
})
.toString()
.trim()
console.log(`Latest tag identified: ${latestTag}`)
console.log()
console.log("Identifying commits since the latest tag...")
const range = `${latestTag}..HEAD`
// Get the commits since the latest tag
const commitsSinceLatestTag = await new Promise<Commit[]>(
(resolve, reject) => {
const stream = gitLog.parse({ _: range })
streamToArray(stream, (err: Error, arr: any[]) => {
if (err) return reject(err)
Promise.all(
arr.map(async (d) => {
const parsed = await parseCommit(d.subject)
return { ...d, parsed }
})
).then((res) => resolve(res.filter(Boolean)))
})
}
)
console.log(commitsSinceLatestTag.length, `commits found since ${latestTag}`)
debug(
"Analyzing the following commits:",
commitsSinceLatestTag.map((c) => ` ${c.subject}`).join("\n")
)
const lastCommit = commitsSinceLatestTag[0]
if (lastCommit?.parsed.raw === RELEASE_COMMIT_MSG) {
debug("Already released...")
return []
}
console.log()
console.log("Identifying commits that touched package code...")
function getChangedFiles(commitSha: string) {
return execSync(
`git diff-tree --no-commit-id --name-only -r ${commitSha}`,
{ stdio: "pipe" }
)
.toString()
.trim()
.split("\n")
}
const packageCommits = commitsSinceLatestTag.filter(({ commit }) => {
const changedFiles = getChangedFiles(commit.short)
return packageFolders.some((packageFolder) =>
changedFiles.some((changedFile) => changedFile.startsWith(packageFolder))
)
})
console.log(packageCommits.length, "commits touched package code")
console.log()
console.log("Identifying packages that need a new release...")
const packagesNeedRelease: string[] = []
const grouppedPackages = packageCommits.reduce((acc, commit) => {
const changedFilesInCommit = getChangedFiles(commit.commit.short)
for (const [pkg, src] of Object.entries(packages)) {
if (
changedFilesInCommit.some((changedFile) => changedFile.startsWith(src))
) {
if (!(pkg in acc)) {
acc[pkg] = { features: [], bugfixes: [], other: [], breaking: [] }
}
const { type } = commit.parsed
if (RELEASE_COMMIT_TYPES.includes(type)) {
if (!packagesNeedRelease.includes(pkg)) {
packagesNeedRelease.push(pkg)
}
if (type === "feat") {
acc[pkg].features.push(commit)
if (commit.body.includes(BREAKING_COMMIT_MSG)) {
const [, changesBody] = commit.body.split(BREAKING_COMMIT_MSG)
acc[pkg].breaking.push({
...commit,
body: changesBody.trim(),
})
}
} else acc[pkg].bugfixes.push(commit)
} else {
acc[pkg].other.push(commit)
}
}
}
return acc
}, {} as Record<string, GrouppedCommits>)
if (packagesNeedRelease.length) {
console.log(
packagesNeedRelease.length,
`new release(s) needed: ${packagesNeedRelease.join(", ")}`
)
} else {
console.log("No packages needed a new release, BYE!")
return []
}
console.log()
const packagesToRelease: PackageToRelease[] = []
for await (const pkgName of packagesNeedRelease) {
const commits = grouppedPackages[pkgName]
const releaseType: semver.ReleaseType = commits.breaking.length
? "major" // 1.x.x
: commits.features.length
? "minor" // x.1.x
: "patch" // x.x.1
const packageJson = await pkgJson.read(packages[pkgName])
const oldVersion = packageJson.version!
const newSemVer = semver.parse(semver.inc(oldVersion, releaseType))!
packagesToRelease.push({
name: pkgName,
oldVersion,
newVersion: `${newSemVer.major}.${newSemVer.minor}.${newSemVer.patch}`,
commits,
path: packages[pkgName],
})
}
return packagesToRelease
}

View File

@@ -1,28 +0,0 @@
export const config = {
releaseBranches: ["main"],
// TODO: Generate dynamically
packages: {
"next-auth": "packages/next-auth",
"@next-auth/dgraph-adapter": "packages/adapter-dgraph",
"@next-auth/fauna-adapter": "packages/adapter-fauna",
"@next-auth/mikro-orm-adapter": "packages/adapter-mikro-orm",
"@next-auth/neo4j-adapter": "packages/adapter-neo4j",
"@next-auth/prisma-adapter": "packages/adapter-prisma",
"@next-auth/upstash-redis-adapter": "packages/adapter-upstash-redis",
"@next-auth/dynamodb-adapter": "packages/adapter-dynamodb",
"@next-auth/firebase-adapter": "packages/adapter-firebase",
"@next-auth/mongodb-adapter": "packages/adapter-mongodb",
"@next-auth/pouchdb-adapter": "packages/adapter-pouchdb",
"@next-auth/sequelize-adapter": "packages/adapter-sequelize",
"@next-auth/typeorm-legacy-adapter": "packages/adapter-typeorm-legacy",
},
rootDir: process.cwd(),
BREAKING_COMMIT_MSG: "BREAKING CHANGE:",
RELEASE_COMMIT_MSG: "chore(release): bump package version(s) [skip ci]",
RELEASE_COMMIT_TYPES: ["feat", "fix"],
dryRun:
!process.env.CI ||
!!process.env.DRY_RUN ||
process.argv.includes("--dry-run"),
verbose: !!process.env.VERBOSE || process.argv.includes("--verbose"),
}

View File

@@ -1,49 +0,0 @@
import { config } from "./config"
import { shouldSkip } from "./skip"
import { verify as verify } from "./verify"
import { analyze } from "./analyze"
import { publish } from "./publish"
import { debug } from "./utils"
async function run() {
if (config.dryRun) {
console.log("\nPerforming dry run, no packages will be published!\n")
}
if (shouldSkip({ releaseBranches: config.releaseBranches })) {
return
}
if (config.dryRun) {
console.log("\nDry run, skip validation...\n")
} else {
await verify()
}
const packages = await analyze(config)
if (!packages.length) return
debug(
"Packages to release:",
packages
.map((p) =>
JSON.stringify(
{
...p,
commits: `${p.commits.features.length} feature(s), ${p.commits.bugfixes.length} bugfixe(s), ${p.commits.other.length} other(s) and ${p.commits.breaking.length} breaking change(s)`,
},
null,
2
)
)
.join("\n")
)
await publish({ ...config, packages })
}
run().catch((err) => {
console.log(err)
process.exit(1)
})

View File

@@ -1,134 +0,0 @@
import type { Commit, PackageToRelease } from "./types"
import { debug, pkgJson, execSync } from "./utils"
export async function publish(options: {
dryRun: boolean
packages: PackageToRelease[]
RELEASE_COMMIT_MSG: string
}) {
const { dryRun, packages, RELEASE_COMMIT_MSG } = options
execSync("pnpm build")
for await (const pkg of packages) {
if (dryRun) {
console.log(
`Dry run, \`npm publish\` would have released package \`${pkg.name}\` with version "${pkg.newVersion}".`
)
} else {
console.log(
`Writing version "${pkg.newVersion}" to package.json for package \`${pkg.name}\``
)
await pkgJson.update(pkg.path, { version: pkg.newVersion })
console.log("package.json file has been written, publishing...")
}
let npmPublish = `pnpm publish --access public --registry=https://registry.npmjs.org --no-git-checks`
// We use different tokens for `next-auth` and `@next-auth/*` packages
if (pkg.name === "next-auth") {
process.env.NPM_TOKEN = process.env.NPM_TOKEN_PKG
} else {
process.env.NPM_TOKEN = process.env.NPM_TOKEN_ORG
}
if (dryRun) {
console.log(
`Dry run, skip \`npm publish\` for package \`${pkg.name}\`...\n`
)
npmPublish += " --dry-run --silent"
} else {
execSync(
"echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc",
{ cwd: pkg.path }
)
}
execSync(npmPublish, { cwd: pkg.path })
}
if (dryRun) {
console.log("Dry run, skip release commit...")
} else {
execSync(`git add -A && git commit -m "${RELEASE_COMMIT_MSG}"`)
console.log("Commited.")
}
for (const pkg of packages) {
const { name, oldVersion, newVersion } = pkg
const gitTag = `${name}@v${newVersion}`
console.log(
`\n\n-------------------------------\n${name} ${oldVersion} -> ${newVersion}`
)
const changelog = createChangelog(pkg)
debug("Changelog generated", changelog)
if (dryRun) {
console.log(`Dry run, skip git tag/release notes for package \`${name}\``)
} else {
console.log(`Creating git tag...`)
execSync(`git tag ${gitTag}`)
execSync("git push --tags")
console.log(`Creating GitHub release notes...`)
execSync(`gh release create ${gitTag} --notes '${changelog}'`)
}
}
if (!dryRun) {
execSync(`git push`)
}
}
function createChangelog(pkg: PackageToRelease) {
const {
commits: { features, breaking, bugfixes, other },
} = pkg
console.log(`Creating changelog for package \`${pkg.name}\`...`)
let changelog = ``
changelog += listGroup("Features", features)
changelog += listGroup("Bugfixes", bugfixes)
changelog += listGroup("Other", other)
if (breaking.length) {
changelog += `
## BREAKING CHANGES
${breaking.map((c) => ` - ${c.body}`).join("\n")}`
}
return changelog
}
function sortByScope(commits: Commit[]) {
return commits.sort((a, b) => {
if (a.parsed.scope && b.parsed.scope) {
return a.parsed.scope.localeCompare(b.parsed.scope)
} else if (a.parsed.scope) return -1
else if (b.parsed.scope) return 1
return a.body.localeCompare(b.body)
})
}
function header(c: Commit) {
let h = c.parsed.subject
if (c.parsed.scope) {
h = `**${c.parsed.scope}**: ${h} (${c.commit.short})`
}
return h
}
function listGroup(heading: string, commits: Commit[]) {
if (!commits.length) return ""
const list = sortByScope(commits)
.map((c) => ` - ${header(c)}`)
.join("\n")
return `## ${heading}
${list}
`
}

View File

@@ -1,18 +0,0 @@
export function shouldSkip(options: { releaseBranches: string[] }) {
if (!process.env.CI) {
return false
}
const { releaseBranches } = options
const branch = process.env.GITHUB_REF_NAME
if (!branch || !releaseBranches.includes(branch)) {
console.log(`\nSkipping release for branch "${branch}"`)
console.log(
`Releases are only triggered for the following branches: ${releaseBranches.join(
", "
)}\n`
)
return true
}
return false
}

View File

@@ -1,12 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"esModuleInterop": true,
"skipLibCheck": true,
"skipDefaultLibCheck": true
},
"ts-node": {
"swc": true
}
}

View File

@@ -1,61 +0,0 @@
export interface Commit {
commit: CommitOrTree
tree: CommitOrTree
author: AuthorOrCommitter
committer: AuthorOrCommitter
subject: string
body: string
parsed: Parsed
}
export interface CommitOrTree {
long: string
short: string
}
export interface AuthorOrCommitter {
name: string
email: string
date: string
}
export interface Parsed {
type: string
scope?: string | null
subject: string
merge?: null
header: string
body?: null
footer?: null
notes?: null[] | null
references?: null[] | null
mentions?: null[] | null
revert?: null
raw: string
}
export interface Package {
name: string
srcDir: string
peerDependencies?: string[]
}
export interface BranchConfig {
prerelease: boolean
ghRelease: boolean
}
export interface PackageToRelease {
name: string
newVersion: string
oldVersion: string
commits: GroupedCommits
path: string
}
export interface GroupedCommits {
features: Commit[]
bugfixes: Commit[]
other: Commit[]
breaking: Commit[]
}

View File

@@ -1,39 +0,0 @@
import type { PackageJson } from "type-fest"
import fs from "node:fs/promises"
import path from "node:path"
import { execSync as nodeExecSync } from "node:child_process"
import { config } from "./config"
async function read(directory: string): Promise<PackageJson> {
const content = await fs.readFile(
path.join(process.cwd(), directory, "package.json"),
"utf8"
)
return JSON.parse(content)
}
async function update(
directory: string,
data: Partial<PackageJson>
): Promise<void> {
const original = await pkgJson.read(directory)
const content = JSON.stringify({ ...original, ...data }, null, 2)
await fs.writeFile(
path.join(process.cwd(), directory, "package.json"),
content,
"utf8"
)
}
export const pkgJson = { read, update }
export function debug(...args: any[]): void {
if (!config.verbose) return
const [first, ...rest] = args
console.log(`\n[debug] ${first}\n`, ...rest, "\n")
}
export function execSync(...args: Parameters<typeof nodeExecSync>) {
return nodeExecSync(args[0], { stdio: "inherit", ...args[1] })
}

View File

@@ -1,11 +0,0 @@
export async function verify() {
if (!process.env.NPM_TOKEN_PKG) {
throw new Error("NPM_TOKEN_PKG is not set")
}
if (!process.env.NPM_TOKEN_ORG) {
throw new Error("NPM_TOKEN_ORG is not set")
}
if (!process.env.RELEASE_TOKEN) {
throw new Error("RELEASE_TOKEN is not set")
}
}