auth 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,32 +1,186 @@
1
- # Auth.js CLI
1
+ <p align="center">
2
+ <img width="96px" src="https://authjs.dev/img/etc/cli.svg" style="margin-right: 16px"/>
3
+ <a href="https://authjs.dev" target="_blank"><img width="96px" src="https://authjs.dev/img/logo-sm.png" /></a>
4
+ <h1 align="center">Auth.js CLI</h1>
5
+ </p>
6
+ <p align="center">
7
+ The CLI tool by <a href="https://authjs.dev">Auth.js</a> to supercharge your authentication workflows.
8
+ </p>
9
+ <p align="center">
10
+ <a href="https://npm.im/auth"><img src="https://img.shields.io/badge/JavaScript-grey?style=flat-square&logo=javascript" alt="JavaScript" /></a>
11
+ <a href="https://www.npmtrends.com/auth"><img src="https://img.shields.io/npm/dm/auth?style=flat-square&color=cyan" alt="Downloads" /></a>
12
+ <a href="https://www.npmjs.com/package/auth"><img src="https://img.shields.io/npm/v/auth?style=flat-square&label=latest&color=purple" alt="npm latest release" /></a>
13
+ <a href="https://github.com/nextauthjs/cli/stargazers"><img src="https://img.shields.io/github/stars/nextauthjs/cli?style=flat-square&color=orange" alt="Github Stars" /></a>
14
+ </p>
2
15
 
3
- The CLI tool by [Auth.js](https://authjs.dev) to supercharge your authentication workflow.
16
+ ## Features
17
+
18
+ - Natural language search to answer questions about Auth.js, docs, APIs, or general auth concepts
19
+ - Initialize new projects with Auth.js for any of the supported frameworks
20
+ - Guides you through adding authentication providers to your existing project
21
+ - ... and more coming soon!
4
22
 
5
23
  ## Installation
6
24
 
7
- You don't need to install this package, run any of the following commands:
25
+ You don't need to install this package, run the following:
8
26
 
9
27
  ```sh
10
- npx auth
28
+ npx auth # or pnpx, bunx, yarn dlx, etc.
11
29
  ```
12
30
 
13
31
  ## Usage
14
32
 
15
- <!-- TODO: Generate by running `node index.js --help` and writing this -->
33
+ <!-- GENERATED START -->
16
34
 
17
- ```sh
35
+ ```
18
36
  Usage: auth [options] [command]
19
37
 
20
38
  Options:
21
- -V, --version output the version number
22
- -h, --help display help for command
39
+ -V, --version output the version number
40
+ -h, --help display help for command
23
41
 
24
42
  Commands:
25
- secret [options] Generate a random string.
26
- framework [framework] Clone a framework template.
27
- help [command] display help for command
43
+ ask [options] Ask about docs, API, or auth concepts.
44
+ init [options] [framework] Initialize a project.
45
+ secret [options] Generate a random string and add it to the .env
46
+ file.
47
+ add [provider] Register a new authentication provider
48
+ help [command] display help for command
28
49
  ```
29
50
 
51
+ <!-- GENERATED END -->
52
+
53
+ ## Security
54
+
55
+ If you think you have found a vulnerability (or are not sure) in Auth.js or any of the related packages (i.e. Adapters), we ask you to read our [Security Policy](https://authjs.dev/security) to reach out responsibly. Please do not open Pull Requests/Issues/Discussions before consulting with us.
56
+
30
57
  ## Acknowledgements
31
58
 
32
59
  Special thanks to Craig for the `auth` package name on npm.
60
+
61
+ ### Sponsors
62
+
63
+ We have an [OpenCollective](https://opencollective.com/nextauth) for companies and individuals looking to contribute financially to the project!
64
+
65
+ <!--sponsors start-->
66
+ <table>
67
+ <tbody>
68
+ <tr>
69
+ <td align="center" valign="top">
70
+ <a href="https://clerk.com?utm_source=sponsorship&utm_medium=github&utm_campaign=authjs&utm_content=sponsor" target="_blank">
71
+ <img width="108" src="https://avatars.githubusercontent.com/u/49538330?s=200&v=4" alt="Clerk Logo" />
72
+ </a><br />
73
+ <div>Clerk</div>
74
+ <sub>💵</sub>
75
+ </td>
76
+ <td align="center" valign="top">
77
+ <a href="https://fusionauth.io" target="_blank">
78
+ <img width="108" src="https://avatars.githubusercontent.com/u/41974756?s=200&v=4" alt="FusionAuth Logo" />
79
+ </a><br />
80
+ <div>FusionAuth</div>
81
+ <sub>💵</sub>
82
+ </td>
83
+ <td align="center" valign="top">
84
+ <a href="https://www.beyondidentity.com" target="_blank">
85
+ <img width="108" src="https://avatars.githubusercontent.com/u/69811361?s=200&v=4" alt="Beyond Identity Logo" />
86
+ </a><br />
87
+ <div>Beyond Identity</div>
88
+ <sub>💵</sub>
89
+ </td>
90
+ <td align="center" valign="top">
91
+ <a href="https://stytch.com" target="_blank">
92
+ <img width="108" src="https://avatars.githubusercontent.com/u/69983493?s=200&v=4" alt="Stytch Logo" />
93
+ </a><br />
94
+ <div>Stytch</div>
95
+ <sub>💵</sub>
96
+ </td>
97
+ <td align="center" valign="top">
98
+ <a href="https://prisma.io" target="_blank">
99
+ <img width="108" src="https://avatars.githubusercontent.com/u/17219288?s=200&v=4" alt="Prisma Logo" />
100
+ </a><br />
101
+ <div>Prisma</div>
102
+ <sub>💵</sub>
103
+ </td>
104
+ <td align="center" valign="top">
105
+ <a href="https://lowdefy.com" target="_blank">
106
+ <img width="108" src="https://avatars.githubusercontent.com/u/47087496?s=200&v=4" alt="Lowdefy Logo" />
107
+ </a><br />
108
+ <div>Lowdefy</div>
109
+ <sub>💵</sub>
110
+ </td>
111
+ <td align="center" valign="top">
112
+ <a href="https://www.descope.com" target="_blank">
113
+ <img width="108" src="https://avatars.githubusercontent.com/u/97479186?s=200&v=4" alt="Descope Logo" />
114
+ </a><br />
115
+ <div>Descope</div>
116
+ <sub>💵</sub>
117
+ </td>
118
+ </tr>
119
+ <tr>
120
+ <td align="center" valign="top">
121
+ <a href="https://badass.dev" target="_blank">
122
+ <img width="108" src="https://avatars.githubusercontent.com/u/136839242?v=4" alt="Badass Courses Logo" />
123
+ </a><br />
124
+ <div>Badass Courses</div>
125
+ <sub>💵</sub>
126
+ </td>
127
+ <td align="center" valign="top">
128
+ <a href="https://github.com/encoredev/encore" target="_blank">
129
+ <img width="108" src="https://avatars.githubusercontent.com/u/50438175?v=4" alt="Encore Logo" />
130
+ </a><br />
131
+ <div>Encore</div>
132
+ <sub>💵</sub>
133
+ </td>
134
+ <td align="center" valign="top">
135
+ <a href="https://arcjet.com/" target="_blank">
136
+ <img width="108" src="https://avatars.githubusercontent.com/u/24397786?s=200&v=4" alt="Arcjet Logo" />
137
+ </a><br />
138
+ <div>Arcjet</div>
139
+ <sub>💵</sub>
140
+ </td>
141
+ <td align="center" valign="top">
142
+ <a href="https://checklyhq.com" target="_blank">
143
+ <img width="108" src="https://avatars.githubusercontent.com/u/25982255?s=200&v=4" alt="Checkly Logo" />
144
+ </a><br />
145
+ <div>Checkly</div>
146
+ <sub>☁️</sub>
147
+ </td>
148
+ <td align="center" valign="top">
149
+ <a href="https://superblog.ai/" target="_blank">
150
+ <img width="108" src="https://d33wubrfki0l68.cloudfront.net/cdc4a3833bd878933fcc131655878dbf226ac1c5/10cd6/images/logo_bolt_small.png" alt="superblog Logo" />
151
+ </a><br />
152
+ <div>superblog</div>
153
+ <sub>☁️</sub>
154
+ </td>
155
+ <td align="center" valign="top">
156
+ <a href="https://www.netlight.com/" target="_blank">
157
+ <img width="108" src="https://avatars.githubusercontent.com/u/1672348?s=200&v=4" alt="Netlight logo" />
158
+ </a><br />
159
+ <div>Netlight</div>
160
+ <sub>☁️</sub>
161
+ </td>
162
+ <td align="center" valign="top">
163
+ <a href="https://vercel.com" target="_blank">
164
+ <img width="108" src="https://avatars.githubusercontent.com/u/14985020?s=200&v=4" alt="Vercel Logo" />
165
+ </a><br />
166
+ <div>Vercel</div>
167
+ <sub>☁️</sub>
168
+ </td>
169
+ </tr>
170
+ </tbody>
171
+ </table>
172
+
173
+ - 💵 Financial Sponsor
174
+ - ☁️ Infrastructure Support
175
+
176
+ <br />
177
+ <!--sponsors end-->
178
+
179
+ ## Contributing
180
+
181
+ We're open to all community contributions! If you'd like to contribute in any way, please first read
182
+ our [Contributing Guide](https://github.com/nextauthjs/.github/blob/main/CONTRIBUTING.md).
183
+
184
+ ## License
185
+
186
+ MIT
@@ -0,0 +1,117 @@
1
+ // @ts-check
2
+
3
+ import * as y from "yoctocolors"
4
+ import open from "open"
5
+ import clipboard from "clipboardy"
6
+ import { select, input, password } from "@inquirer/prompts"
7
+ import { requireFramework } from "../lib/detect.js"
8
+ import { updateEnvFile } from "../lib/write-env.js"
9
+ import { providers, frameworks } from "../lib/meta.js"
10
+ import { secret } from "./index.js"
11
+ import { link, markdownToAnsi } from "../lib/markdown.js"
12
+
13
+ const choices = Object.entries(providers)
14
+ .filter(([, { setupUrl }]) => !!setupUrl)
15
+ .map(([value, { name }]) => ({ name, value }))
16
+
17
+ /** @param {string | undefined} providerId */
18
+ export async function action(providerId) {
19
+ try {
20
+ if (!providerId) {
21
+ providerId = await select({
22
+ message: "What provider do you want to set up?",
23
+ choices: choices,
24
+ })
25
+ }
26
+
27
+ const provider = providers[providerId]
28
+ if (!provider?.setupUrl) {
29
+ console.error(
30
+ y.red(
31
+ `Missing instructions for ${
32
+ provider?.name ?? providerId
33
+ }.\nInstructions are available for: ${y.bold(
34
+ choices.map((choice) => choice.name).join(", ")
35
+ )}`
36
+ )
37
+ )
38
+ return
39
+ }
40
+
41
+ const frameworkId = await requireFramework()
42
+ const framework = frameworks[frameworkId]
43
+
44
+ console.log(
45
+ y.dim(
46
+ `Setting up OAuth provider in your ${framework.name} app (${link(
47
+ "more info",
48
+ "https://authjs.dev/getting-started/authentication/oauth"
49
+ )})...\n`
50
+ )
51
+ )
52
+
53
+ const url = new URL(
54
+ `${framework.path}/callback/${providerId}`,
55
+ `http://localhost:${framework.port}`
56
+ )
57
+
58
+ clipboard.writeSync(url.toString())
59
+ console.log(
60
+ `\
61
+ ${y.bold("Setup URL")}: ${provider.setupUrl}
62
+ ${y.bold("Callback URL (copied to clipboard)")}: ${url}`
63
+ )
64
+
65
+ console.log("_________________________")
66
+ if (provider.instructions) {
67
+ console.log(y.dim("\nFollow the instructions:\n"))
68
+ console.log(markdownToAnsi(provider.instructions))
69
+ }
70
+ console.log(
71
+ y.dim(
72
+ `\nProvider documentation: https://providers.authjs.dev/${providerId}\n`
73
+ )
74
+ )
75
+ console.log("‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾")
76
+ console.log(y.dim("Opening setup URL in your browser...\n"))
77
+ await new Promise((resolve) => setTimeout(resolve, 3000))
78
+
79
+ await open(provider.setupUrl)
80
+
81
+ const clientId = await input({
82
+ message: `Paste ${y.magenta("Client ID")}:`,
83
+ validate: (value) => !!value,
84
+ })
85
+ const clientSecret = await password({
86
+ message: `Paste ${y.magenta("Client secret")}:`,
87
+ mask: true,
88
+ validate: (value) => !!value,
89
+ })
90
+
91
+ console.log(y.dim(`Updating environment variable file...`))
92
+
93
+ const varPrefix = `AUTH_${providerId.toUpperCase()}`
94
+
95
+ await updateEnvFile({
96
+ [`${varPrefix}_ID`]: clientId,
97
+ [`${varPrefix}_SECRET`]: clientSecret,
98
+ })
99
+
100
+ console.log(
101
+ y.dim(
102
+ `\nEnsuring that ${link(
103
+ "AUTH_SECRET",
104
+ "https://authjs.dev/getting-started/installation#setup-environment"
105
+ )} is set...`
106
+ )
107
+ )
108
+
109
+ await secret.action({})
110
+
111
+ console.log("\n🎉 Done! You can now use this provider in your app.")
112
+ } catch (error) {
113
+ if (!(error instanceof Error)) return
114
+ if (error.message.startsWith("User force closed")) return
115
+ throw error
116
+ }
117
+ }
@@ -0,0 +1,90 @@
1
+ // @ts-check
2
+
3
+ import * as y from "yoctocolors"
4
+ import { input } from "@inquirer/prompts"
5
+ import { InkeepAI } from "@inkeep/ai-api"
6
+ import { ChatModeOptions } from "@inkeep/ai-api/models/components/index.js"
7
+ import * as ora from "ora"
8
+ import { link, markdownToAnsi, breakStringToLines } from "../lib/markdown.js"
9
+
10
+ const INKEEP_API_KEY = "e32967a320a48a2cd933922099e1f38f6ebb4ab62ff98343"
11
+ const INKEEP_INTEGRATION_ID = "clvn0fdez000cip0e5w2oaobw"
12
+
13
+ const inkeepAI = new InkeepAI({ apiKey: INKEEP_API_KEY })
14
+
15
+ function randomSpinner() {
16
+ const allSpinners = Object.keys(ora.spinners)
17
+ const max = allSpinners.length - 1
18
+ const random = Math.floor(Math.random() * (max + 1))
19
+ return allSpinners[random]
20
+ }
21
+
22
+ export async function action({ stream = false, raw = false }) {
23
+ const format = raw ? (i) => i : markdownToAnsi
24
+ const newLocal = randomSpinner()
25
+ const spinner = ora.default({
26
+ text: "Understanding question",
27
+ // @ts-expect-error
28
+ spinner: newLocal,
29
+ })
30
+ try {
31
+ const content = await input({ message: "What would you like to know: " })
32
+
33
+ spinner.start()
34
+
35
+ const result = await inkeepAI.chatSession.create({
36
+ integrationId: INKEEP_INTEGRATION_ID,
37
+ stream,
38
+ chatSession: { messages: [{ content, role: "user" }] },
39
+ chatMode: ChatModeOptions.Auto,
40
+ })
41
+
42
+ if (stream) {
43
+ if (result.chatResultStream == null) {
44
+ throw new Error("failed to create stream: received null value")
45
+ }
46
+
47
+ spinner.stop().clear()
48
+
49
+ for await (const event of result.chatResultStream) {
50
+ if (event.event === "message_chunk") {
51
+ const content = format(event.data.contentChunk)
52
+ process.stdout.write(content)
53
+ }
54
+ }
55
+ } else {
56
+ const answer = result.chatResult?.message.content
57
+ if (!answer) return console.error(y.red("Could not get an answer."))
58
+
59
+ console.log(breakStringToLines(format(answer), 80))
60
+ }
61
+
62
+ console.log(
63
+ y.italic(
64
+ `\n\nAnswered by ${y.cyan(
65
+ link("Inkeep AI", "https://inkeep.com")
66
+ )}. Check out the official ${y.magenta(
67
+ link("Auth.js", "https://authjs.dev")
68
+ )} docs.`
69
+ )
70
+ )
71
+ } catch (e) {
72
+ spinner.stop().clear()
73
+ if (!(e instanceof Error)) return
74
+ if (e.message.startsWith("User force closed")) return
75
+
76
+ console.error(y.red(e.stack ?? e.message))
77
+ console.error(
78
+ y.bold(
79
+ y.red(
80
+ `\n This is a bug. Please report it on ${link(
81
+ "GitHub",
82
+ "https://github.com/nextauthjs/auth-cli/issues"
83
+ )}.`
84
+ )
85
+ )
86
+ )
87
+ } finally {
88
+ spinner.stop().clear()
89
+ }
90
+ }
@@ -0,0 +1,4 @@
1
+ export * as ask from "./ask.js"
2
+ export * as init from "./init.js"
3
+ export * as secret from "./secret.js"
4
+ export * as add from './add.js'
@@ -0,0 +1,121 @@
1
+ // @ts-check
2
+ import { execSync } from "node:child_process"
3
+ import { input, confirm, select, checkbox } from "@inquirer/prompts"
4
+ import * as y from "yoctocolors"
5
+ import { action as secretAction } from "./secret.js"
6
+
7
+ import { getPkgManager, install } from "../lib/pkg-manager.js"
8
+ import * as meta from "../lib/meta.js"
9
+ import { updateEnvFile } from "../lib/write-env.js"
10
+ import { mkdir } from "node:fs/promises"
11
+
12
+ /**
13
+ * @param {string} framework
14
+ * @param {{example?: boolean}} options
15
+ * @returns
16
+ */
17
+ export async function action(framework, options) {
18
+ try {
19
+ if (!framework) {
20
+ framework = await select({
21
+ message: "Select a framework",
22
+ choices: Object.entries(meta.frameworks).map(([key, value]) => ({
23
+ name: `${value.name} ${y.dim(`(${key})`)}`,
24
+ value: key,
25
+ })),
26
+ })
27
+ }
28
+
29
+ const dir = await input({
30
+ message: "Name of the project",
31
+ default: `my-${framework}-app`,
32
+ })
33
+
34
+ if (options.example) await createFromExample(framework, dir)
35
+
36
+ const providers = await checkbox({
37
+ instructions: false,
38
+ message: "Select one or multiple providers",
39
+ choices: Object.entries(meta.providers).map(([value, { name }]) => ({
40
+ name,
41
+ value,
42
+ })),
43
+ })
44
+
45
+ /** @type {string|undefined} */
46
+ let adapter = await select({
47
+ default: "none",
48
+ message: "Select a database adapter",
49
+ choices: Object.entries(meta.adapters).map(([value, name]) => ({
50
+ name,
51
+ value,
52
+ })),
53
+ })
54
+ adapter = adapter === "none" ? undefined : adapter
55
+
56
+ await scaffoldProject({ framework, dir, providers, adapter })
57
+ } catch (e) {
58
+ if (!(e instanceof Error)) return
59
+ if (e.message.startsWith("User force closed")) return
60
+
61
+ console.error(y.red(e.message))
62
+ }
63
+ }
64
+
65
+ /**
66
+ * @param {string} framework
67
+ * @param {string} dir
68
+ */
69
+ async function createFromExample(framework, dir) {
70
+ const { src, demo } = meta.frameworks[framework]
71
+ execSync(`git clone ${src} ${dir}`)
72
+ if (
73
+ await confirm({
74
+ message: "Initiailze .env with `AUTH_SECRET`",
75
+ default: true,
76
+ })
77
+ ) {
78
+ execSync(`cd ${dir}`)
79
+ await secretAction({ path: dir })
80
+ }
81
+
82
+ await install()
83
+
84
+ console.log(`
85
+ Project ${y.italic(`\`${dir}\``)} has been created.
86
+ Source code: ${src}
87
+ Deployed demo: ${demo}`)
88
+ }
89
+
90
+ /**
91
+ * @param {{framework: string, dir: string, providers: string[], adapter?: string}} options
92
+ */
93
+ async function scaffoldProject(options) {
94
+ const { framework, providers, dir, adapter } = options
95
+ const pkgManager = getPkgManager()
96
+ console.log(
97
+ `Scaffolding ${y.bold(framework)} project with ${y.bold(
98
+ pkgManager
99
+ )} at ${y.bold(dir)}...`
100
+ )
101
+ throw new Error("Scaffolding not implemented. Please use `init --example`")
102
+ console.log(`Create directory ${dir}`)
103
+ await mkdir(dir)
104
+ console.log(`Change directory to ${dir}`)
105
+ execSync(`cd ${dir}`)
106
+ console.log(`Initialize ${pkgManager} project`)
107
+ execSync(`${pkgManager} init`)
108
+ // console.log(`Add ${framework} to ${pkgManager}`)
109
+ // execSync(`${pkgManager} install ${framework}`)
110
+ for (const provider of providers) {
111
+ const id = `AUTH_${provider.toUpperCase()}_ID`
112
+ const secret = `AUTH_${provider.toUpperCase()}_SECRET`
113
+ await updateEnvFile(
114
+ {
115
+ [id]: "",
116
+ [secret]: "",
117
+ },
118
+ dir
119
+ )
120
+ }
121
+ }
@@ -0,0 +1,55 @@
1
+ // @ts-check
2
+
3
+ import * as y from "yoctocolors"
4
+ import clipboard from "clipboardy"
5
+ import { requireFramework } from "../lib/detect.js"
6
+ import { updateEnvFile } from "../lib/write-env.js"
7
+
8
+ /** Web compatible method to create a random string of a given length */
9
+ function randomString(size = 32) {
10
+ const bytes = crypto.getRandomValues(new Uint8Array(size))
11
+ // @ts-expect-error
12
+ return Buffer.from(bytes, "base64").toString("base64")
13
+ }
14
+
15
+ /**
16
+ * @param {{
17
+ * copy?: boolean
18
+ * path?: string
19
+ * raw?: boolean
20
+ * }} options
21
+ */
22
+ export async function action(options) {
23
+ const key = "AUTH_SECRET"
24
+ const value = randomString()
25
+ if (options.raw) return console.log(value)
26
+
27
+ const line = `${key}="${value}"`
28
+ const message = {
29
+ introClipboard:
30
+ "Secret generated. It has been copied to your clipboard, paste it to your .env/.env.local file to continue.",
31
+ introCopy:
32
+ "Secret generated. Copy it to your .env/.env.local file (depending on your framework):",
33
+ value: `\n${line}`,
34
+ }
35
+
36
+ if (options.copy) {
37
+ try {
38
+ clipboard.writeSync(line)
39
+ console.log(message.introClipboard)
40
+ } catch (error) {
41
+ console.error(y.red(error))
42
+ console.log(message.introCopy)
43
+ } finally {
44
+ console.log(message.value)
45
+ process.exit(0)
46
+ }
47
+ }
48
+
49
+ try {
50
+ await requireFramework(options.path)
51
+ await updateEnvFile({ [key]: value }, options.path, true)
52
+ } catch (error) {
53
+ console.error(y.red(error))
54
+ }
55
+ }
package/index.js CHANGED
@@ -1,80 +1,63 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { InvalidArgumentError } from "commander"
4
- import { Command } from "commander"
3
+ // @ts-check
4
+
5
+ import { Command, InvalidArgumentError } from "commander"
6
+ import * as y from "yoctocolors"
7
+ import { ask, init, secret, add } from "./commands/index.js"
8
+
5
9
  // import pkg from "./package.json" assert { type: "json" }
6
10
 
7
- import fs from "fs/promises"
8
- import { join } from "path"
9
- import { fileURLToPath } from "url"
11
+ import fs from "node:fs/promises"
12
+ import { join } from "node:path"
13
+ import { fileURLToPath } from "node:url"
14
+
10
15
  const __dirname = fileURLToPath(new URL(".", import.meta.url))
11
- const pkg = JSON.parse(await fs.readFile(join(__dirname, "./package.json")))
16
+ const pkg = JSON.parse(
17
+ await fs.readFile(join(__dirname, "./package.json"), "utf-8")
18
+ )
12
19
  const { name, description, version } = pkg
13
20
 
14
21
  try {
15
22
  // TODO: Remove when Node.js 18 is not maintained anymore
23
+ // @ts-expect-error
16
24
  globalThis.crypto ??= (await import("crypto")).webcrypto
17
25
  } catch {}
18
26
 
19
- /** Web compatible method to create a random string of a given length */
20
- export function randomString(size = 32) {
21
- const bytes = crypto.getRandomValues(new Uint8Array(size))
22
- return Buffer.from(bytes, "base64").toString("base64")
23
- }
24
-
25
27
  const program = new Command()
26
28
 
27
- program.name(name).description(description).version(version)
29
+ program
30
+ .name(name)
31
+ .description(description.replace("Auth.js", y.magenta(y.bold("Auth.js"))))
32
+ .version(version)
28
33
 
29
34
  program
30
- .command("secret")
31
- .option("--raw", "Output the string without any formatting.")
32
- .description("Generate a random string.")
33
- .action((options) => {
34
- const value = randomString()
35
- if (options.raw) return console.log(value)
36
- // TODO: Detect framework, check for existing value, and write automatically
37
- console.log(`
38
- Secret generated. Copy it to your .env/.env.local file (depending on your framework):
35
+ .command("ask")
36
+ .option("--stream", "Stream the response.")
37
+ .option("--raw", "Show the Markdown response without formatting.")
38
+ .description("Ask about docs, API, or auth concepts.")
39
+ .action(ask.action)
39
40
 
40
- AUTH_SECRET=${value}`)
41
- })
41
+ program
42
+ .command("init")
43
+ .argument("[framework]", "The framework to use.")
44
+ .option("-e, --example", "Clone a full example.")
45
+ .description("Initialize a project.")
46
+ .action(init.action)
42
47
 
43
- // TODO: Get this programmatically
44
- const frameworks = {
45
- nextjs: {
46
- src: "https://github.com/nextauthjs/next-auth-example",
47
- demo: "https://next-auth-example.vercel.app",
48
- },
49
- sveltekit: {
50
- src: "https://github.com/nextauthjs/sveltekit-auth-example",
51
- demo: "https://sveltekit-auth-example.vercel.app",
52
- },
53
- express: {
54
- src: "https://github.com/nextauthjs/express-auth-example",
55
- demo: "https://express-auth-example.vercel.app",
56
- },
57
- }
48
+ program
49
+ .command("secret")
50
+ .option("--raw", "Output the string without any formatting.")
51
+ .option("--copy", 'Copy AUTH_SECRET="value".')
52
+ .description("Generate a random string and add it to the .env file.")
53
+ .action(secret.action)
58
54
 
59
55
  program
60
- .command("framework")
61
- .argument("[framework]", "The framework to use.", (value) => {
62
- if (!value) return value
63
- if (Object.keys(frameworks).includes(value)) return value
64
- throw new InvalidArgumentError(
65
- `Valid frameworks are: ${supportedFrameworks.join(", ")}`
66
- )
67
- })
68
- .description("Clone a framework template.")
69
- .action((framework) => {
70
- if (!framework) {
71
- return console.log(`
72
- Supported frameworks are: ${Object.keys(frameworks).join(", ")}`)
73
- }
74
- const { src, demo } = frameworks[framework]
75
- console.log(`
76
- Source code: ${src}
77
- Deployed demo: ${demo}`)
78
- })
56
+ .command("add")
57
+ .argument("[provider]", "The authentication provider.")
58
+ .description('Register a new authentication provider')
59
+ .action(add.action)
60
+
61
+ program.parse()
79
62
 
80
- program.parse(process.argv)
63
+ export { program }
package/lib/detect.js ADDED
@@ -0,0 +1,56 @@
1
+ // @ts-check
2
+ import { readFile } from "node:fs/promises"
3
+ import { join } from "node:path"
4
+ import { frameworks } from "../lib/meta.js"
5
+ import * as y from "yoctocolors"
6
+
7
+ /**
8
+ * When this function runs in a framework directory we support,
9
+ * it will return the framework's name
10
+ * @param {string} path
11
+ * @returns {Promise<import("./meta").SupportedFramework | "unknown">}
12
+ */
13
+ export async function detectFramework(path = "") {
14
+ const dir = process.cwd()
15
+ const packageJsonPath = join(dir, path, "package.json")
16
+ try {
17
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"))
18
+
19
+ /** @type {import("./meta").SupportedFramework[]} */
20
+ const foundFrameworks = []
21
+
22
+ if (packageJson.dependencies["next"]) foundFrameworks.push("next")
23
+ if (packageJson.dependencies["express"]) foundFrameworks.push("express")
24
+ if (packageJson.devDependencies["@sveltejs/kit"])
25
+ foundFrameworks.push("sveltekit")
26
+
27
+ if (foundFrameworks.length === 1) return foundFrameworks[0]
28
+
29
+ if (foundFrameworks.length > 1) {
30
+ console.error(
31
+ `Multiple supported frameworks detected: ${foundFrameworks.join(", ")}`
32
+ )
33
+ return "unknown"
34
+ }
35
+ return "unknown"
36
+ } catch {
37
+ return "unknown"
38
+ }
39
+ }
40
+
41
+ export async function requireFramework(path = "") {
42
+ const framework = await detectFramework(path)
43
+
44
+ if (framework === "unknown") {
45
+ console.error(
46
+ y.red(`No framework detected. Currently supported frameworks are: ${y.bold(
47
+ Object.keys(frameworks).join(", ")
48
+ )}`)
49
+ )
50
+
51
+ process.exit(0)
52
+ }
53
+
54
+ return framework
55
+ }
56
+
@@ -0,0 +1,46 @@
1
+ // @ts-check
2
+
3
+ import { execSync } from "node:child_process"
4
+ import dns from "node:dns/promises"
5
+
6
+ function getProxy() {
7
+ if (process.env.https_proxy) {
8
+ return process.env.https_proxy
9
+ }
10
+
11
+ try {
12
+ const httpsProxy = execSync("npm config get https-proxy").toString().trim()
13
+ return httpsProxy !== "null" ? httpsProxy : undefined
14
+ } catch (e) {
15
+ return
16
+ }
17
+ }
18
+
19
+ export async function getOnline() {
20
+ try {
21
+ await dns.lookup("registry.yarnpkg.com")
22
+ // If DNS lookup succeeds, we are online
23
+ return true
24
+ } catch {
25
+ // The DNS lookup failed, but we are still fine as long as a proxy has been set
26
+ const proxy = getProxy()
27
+ if (!proxy) {
28
+ return false
29
+ }
30
+
31
+ const { hostname } = new URL(proxy)
32
+ if (!hostname) {
33
+ // Invalid proxy URL
34
+ return false
35
+ }
36
+
37
+ try {
38
+ await dns.lookup(hostname)
39
+ // If DNS lookup succeeds for the proxy server, we are online
40
+ return true
41
+ } catch {
42
+ // The DNS lookup for the proxy server also failed, so we are offline
43
+ return false
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,59 @@
1
+ // @ts-check
2
+ import * as y from "yoctocolors"
3
+
4
+ // double char markdown matchers
5
+ const BOLD_REGEX = /\*{2}([^*]+)\*{2}/g
6
+ const UNDERLINE_REGEX = /_{2}([^_]+)_{2}/g
7
+ const STRIKETHROUGH_REGEX = /~{2}([^~]+)~{2}/g
8
+ const LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g
9
+
10
+ // single char markdown matchers
11
+ const ITALIC_REGEX = /(?<!\\)\*(.+)(?<!\\)\*|(?<!\\)_(.+)(?<!\\)_/g
12
+
13
+ export function link(text, url) {
14
+ return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`
15
+ }
16
+
17
+ /**
18
+ * @param {string} input
19
+ * @returns {string}
20
+ */
21
+ export function markdownToAnsi(input) {
22
+ input = input.replace(BOLD_REGEX, (...args) => y.bold(args[1]))
23
+ input = input.replace(UNDERLINE_REGEX, (...args) => y.underline(args[1]))
24
+ input = input.replace(STRIKETHROUGH_REGEX, (...args) =>
25
+ y.strikethrough(args[1])
26
+ )
27
+ input = input.replace(ITALIC_REGEX, (...args) => y.italic(args[1] || args[2]))
28
+ input = input.replace(/(?<!\\)\\/g, "")
29
+
30
+ // @ts-expect-error
31
+ input = input.replaceAll(LINK_REGEX, (...args) =>
32
+ y.blue(" " + link(args[2], args[1]))
33
+ )
34
+ return input
35
+ }
36
+
37
+ /**
38
+ * @param {string} str
39
+ * @param {number} maxLineLength
40
+ * @returns {string}
41
+ */
42
+ export function breakStringToLines(str, maxLineLength) {
43
+ let result = ""
44
+ let line = ""
45
+
46
+ str.split(" ").forEach((word) => {
47
+ if (line.length + word.length + 1 > maxLineLength) {
48
+ result += line + "\n"
49
+ line = word
50
+ } else {
51
+ if (line) line += " "
52
+ line += word
53
+ }
54
+ })
55
+
56
+ if (line) result += line
57
+
58
+ return result
59
+ }
package/lib/meta.js ADDED
@@ -0,0 +1,173 @@
1
+ // @ts-check
2
+
3
+ // TODO: Get these programmatically
4
+
5
+ /**
6
+ * @typedef {"next" | "express" | "sveltekit"} SupportedFramework
7
+ */
8
+
9
+ export const frameworks = {
10
+ next: {
11
+ name: "Next.js",
12
+ src: "https://github.com/nextauthjs/next-auth-example",
13
+ demo: "https://next-auth-example.vercel.app",
14
+ path: "/api/auth",
15
+ port: 3000,
16
+ envFile: ".env.local",
17
+ },
18
+ sveltekit: {
19
+ name: "SvelteKit",
20
+ src: "https://github.com/nextauthjs/sveltekit-auth-example",
21
+ demo: "https://sveltekit-auth-example.vercel.app",
22
+ path: "/auth",
23
+ port: 5173,
24
+ envFile: ".env",
25
+ },
26
+ express: {
27
+ name: "Express",
28
+ src: "https://github.com/nextauthjs/express-auth-example",
29
+ demo: "https://express-auth-example.vercel.app",
30
+ path: "/auth",
31
+ port: 3000,
32
+ envFile: ".env",
33
+ },
34
+ }
35
+
36
+ export const providers = {
37
+ "42-school": { name: "42 School", setupUrl: undefined },
38
+ apple: {
39
+ name: "Apple",
40
+ setupUrl:
41
+ "https://developer.apple.com/account/resources/identifiers/list/serviceId",
42
+ },
43
+ asgardeo: { name: "Asgardeo", setupUrl: undefined },
44
+ auth0: { name: "Auth0", setupUrl: undefined },
45
+ authentik: { name: "Authentik", setupUrl: undefined },
46
+ "azure-ad-b2c": { name: "Azure AD B2C", setupUrl: undefined },
47
+ "azure-ad": { name: "Azure AD", setupUrl: undefined },
48
+ "azure-devops": { name: "Azure DevOps", setupUrl: undefined },
49
+ battlenet: { name: "Battlenet", setupUrl: undefined },
50
+ beyondidentity: { name: "Beyond Identity", setupUrl: undefined },
51
+ box: { name: "Box", setupUrl: undefined },
52
+ "boxyhq-saml": { name: "Boxyhq SAML", setupUrl: undefined },
53
+ bungie: { name: "Bungie", setupUrl: undefined },
54
+ "click-up": { name: "Click-up", setupUrl: undefined },
55
+ cognito: { name: "Cognito", setupUrl: undefined },
56
+ coinbase: { name: "Coinbase", setupUrl: undefined },
57
+ credentials: { name: "Credentials", setupUrl: undefined },
58
+ descope: { name: "Descope", setupUrl: undefined },
59
+ discord: { name: "Discord", setupUrl: undefined },
60
+ dribbble: { name: "Dribbble", setupUrl: undefined },
61
+ dropbox: { name: "Dropbox", setupUrl: undefined },
62
+ "duende-identity-server6": {
63
+ name: "Duende Identity Server 6",
64
+ setupUrl: undefined,
65
+ },
66
+ email: { name: "Email", setupUrl: undefined },
67
+ eveonline: { name: "Eveonline", setupUrl: undefined },
68
+ facebook: { name: "Facebook", setupUrl: undefined },
69
+ faceit: { name: "Faceit", setupUrl: undefined },
70
+ foursquare: { name: "Foursquare", setupUrl: undefined },
71
+ freshbooks: { name: "Freshbooks", setupUrl: undefined },
72
+ fusionauth: { name: "FusionAuth", setupUrl: undefined },
73
+ github: {
74
+ name: "GitHub",
75
+ setupUrl: "https://github.com/settings/applications/new",
76
+ instructions: `\
77
+ 1. Set *Application name* (can be anything)
78
+ 2. Set *Homepage URL* (your business/website, but can be anything)
79
+ 3. Paste the redirect URI (on your clipboard) to *Authorization callback URL*
80
+ 4. Click *Register application*
81
+ 5. Paste the *Client ID* back here
82
+ 6. Click *Generate a new client secret*
83
+ 7. Paste the *Client secret* back here (Note: This is the only time you can see it)`,
84
+ },
85
+ gitlab: { name: "GitLab", setupUrl: undefined },
86
+ google: {
87
+ name: "Google",
88
+ setupUrl: "https://console.cloud.google.com/apis/credentials/oauthclient",
89
+ instructions: `\
90
+ 1. Choose *Application Type: Web Application*
91
+ 2. Paste the redirect URI (on your clipboard) to *Authorized redirect URIs*
92
+ 3. Fill out the rest of the form
93
+ 4. Click *Create*`,
94
+ },
95
+ hubspot: { name: "Hubspot", setupUrl: undefined },
96
+ "identity-server4": { name: "Identity Server 4", setupUrl: undefined },
97
+ instagram: { name: "Instagram", setupUrl: undefined },
98
+ kakao: { name: "Kakao", setupUrl: undefined },
99
+ keycloak: { name: "Keycloak", setupUrl: undefined },
100
+ line: { name: "Line", setupUrl: undefined },
101
+ linkedin: { name: "LinkedIn", setupUrl: undefined },
102
+ mailchimp: { name: "Mailchimp", setupUrl: undefined },
103
+ mailru: { name: "Mail.ru", setupUrl: undefined },
104
+ mastodon: { name: "Mastodon", setupUrl: undefined },
105
+ mattermost: { name: "Mattermost", setupUrl: undefined },
106
+ medium: { name: "Medium", setupUrl: undefined },
107
+ "microsoft-entra-id": { name: "Microsoft Entra ID", setupUrl: undefined },
108
+ naver: { name: "Naver", setupUrl: undefined },
109
+ netlify: { name: "Netlify", setupUrl: undefined },
110
+ netsuite: { name: "Netsuite", setupUrl: undefined },
111
+ nodemailer: { name: "Nodemailer", setupUrl: undefined },
112
+ notion: { name: "Notion", setupUrl: undefined },
113
+ okta: { name: "Okta", setupUrl: undefined },
114
+ onelogin: { name: "Onelogin", setupUrl: undefined },
115
+ "ory-hydra": { name: "Ory Hydra", setupUrl: undefined },
116
+ osso: { name: "Osso", setupUrl: undefined },
117
+ osu: { name: "Osu", setupUrl: undefined },
118
+ passage: { name: "Passage", setupUrl: undefined },
119
+ passkey: { name: "Passkey", setupUrl: undefined },
120
+ patreon: { name: "Patreon", setupUrl: undefined },
121
+ pinterest: { name: "Pinterest", setupUrl: undefined },
122
+ pipedrive: { name: "Pipedrive", setupUrl: undefined },
123
+ postmark: { name: "Postmark", setupUrl: undefined },
124
+ reddit: { name: "Reddit", setupUrl: undefined },
125
+ resend: { name: "Resend", setupUrl: undefined },
126
+ salesforce: { name: "Salesforce", setupUrl: undefined },
127
+ sendgrid: { name: "Sendgrid", setupUrl: undefined },
128
+ slack: { name: "Slack", setupUrl: undefined },
129
+ spotify: { name: "Spotify", setupUrl: undefined },
130
+ strava: { name: "Strava", setupUrl: undefined },
131
+ tiktok: { name: "Tiktok", setupUrl: undefined },
132
+ todoist: { name: "Todoist", setupUrl: undefined },
133
+ trakt: { name: "Trakt", setupUrl: undefined },
134
+ twitch: { name: "Twitch", setupUrl: undefined },
135
+ twitter: { name: "Twitter", setupUrl: undefined },
136
+ "united-effects": { name: "United Effects", setupUrl: undefined },
137
+ vk: { name: "Vk", setupUrl: undefined },
138
+ webex: { name: "Webex", setupUrl: undefined },
139
+ wikimedia: { name: "Wikimedia", setupUrl: undefined },
140
+ wordpress: { name: "Wordpress", setupUrl: undefined },
141
+ workos: { name: "WorkOS", setupUrl: undefined },
142
+ yandex: { name: "Yandex", setupUrl: undefined },
143
+ zitadel: { name: "Zitadel", setupUrl: undefined },
144
+ zoho: { name: "Zoho", setupUrl: undefined },
145
+ zoom: { name: "Zoom", setupUrl: undefined },
146
+ }
147
+
148
+ export const adapters = {
149
+ none: "None",
150
+ "adapter-azure-tables": "Azure Tables",
151
+ d1: "d1",
152
+ dgraph: "Dgraph",
153
+ drizzle: "Drizzle",
154
+ dynamodb: "DynamoDB",
155
+ edgedb: "EdgeDB",
156
+ fauna: "Fauna",
157
+ firebase: "Firebase",
158
+ hasura: "Hasura",
159
+ kysely: "Kysely",
160
+ "mikro-orm": "MikroORM",
161
+ mongodb: "MongoDB",
162
+ neo4j: "Neo4j",
163
+ pg: "PostgreSQL",
164
+ pouchdb: "PouchDB",
165
+ prisma: "Prisma",
166
+ sequelize: "Sequelize",
167
+ supabase: "Supabase",
168
+ surrealdb: "SurrealDB",
169
+ typeorm: "TypeORM",
170
+ unstorage: "Unstorage",
171
+ "upstash-redis": "Upstash Redis",
172
+ xata: "Xata",
173
+ }
@@ -0,0 +1,70 @@
1
+ // @ts-check
2
+
3
+ import { yellow } from "yoctocolors"
4
+ import { spawn } from "node:child_process"
5
+ import { getOnline } from "./is-online.js"
6
+
7
+ /**
8
+ * @typedef {'npm' | 'pnpm' | 'yarn' | 'bun'} PackageManager
9
+ */
10
+
11
+ /**
12
+ * @source https://github.com/vercel/next.js/blob/canary/packages/create-next-app/helpers/get-pkg-manager.ts
13
+ * @returns {PackageManager}
14
+ */
15
+ export function getPkgManager() {
16
+ const userAgent = process.env.npm_config_user_agent || ""
17
+
18
+ if (userAgent.startsWith("pnpm")) {
19
+ return "pnpm"
20
+ }
21
+
22
+ if (userAgent.startsWith("bun")) {
23
+ return "bun"
24
+ }
25
+
26
+ if (userAgent.startsWith("yarn")) {
27
+ return "yarn"
28
+ }
29
+
30
+ return "npm"
31
+ }
32
+
33
+ /**
34
+ * @source https://github.com/vercel/next.js/blob/canary/packages/create-next-app/helpers/install.ts
35
+ * Spawn a package manager installation based on user preference.
36
+ *
37
+ * @returns A Promise that resolves once the installation is finished.
38
+ */
39
+ export async function install() {
40
+ const packageManager = getPkgManager()
41
+ /** @type {string[]} */
42
+ const args = ["install"]
43
+ if (!(await getOnline())) {
44
+ console.log(
45
+ yellow("You appear to be offline.\nFalling back to the local cache.")
46
+ )
47
+ args.push("--offline")
48
+ }
49
+ return new Promise((resolve, reject) => {
50
+ /** Spawn the installation process. */
51
+ const child = spawn(packageManager, args, {
52
+ stdio: "inherit",
53
+ env: {
54
+ ...process.env,
55
+ ADBLOCK: "1",
56
+ // we set NODE_ENV to development as pnpm skips dev
57
+ // dependencies when production
58
+ NODE_ENV: "development",
59
+ DISABLE_OPENCOLLECTIVE: "1",
60
+ },
61
+ })
62
+ child.on("close", (code) => {
63
+ if (code !== 0) {
64
+ reject({ command: `${packageManager} ${args.join(" ")}` })
65
+ return
66
+ }
67
+ resolve(void 0)
68
+ })
69
+ })
70
+ }
@@ -0,0 +1,61 @@
1
+ // @ts-check
2
+
3
+ import * as y from "yoctocolors"
4
+ import { readFile, writeFile } from "node:fs/promises"
5
+ import prompt from "prompts"
6
+ import { join } from "node:path"
7
+ import { frameworks } from "./meta.js"
8
+ import { detectFramework } from "./detect.js"
9
+
10
+ /**
11
+ * Add/update key-value pair(s) to a .env file
12
+ * @param {Record<string, string>} env
13
+ * @param {string|undefined} envPath
14
+ * @param {boolean} comment
15
+ */
16
+ export async function updateEnvFile(env, envPath = "", comment = false) {
17
+ const framework = await detectFramework(envPath)
18
+ const dotEnvFile = frameworks[framework]?.envFile
19
+ const file = join(process.cwd(), envPath, dotEnvFile)
20
+ let content = ""
21
+ let read = false
22
+ let created = false
23
+ for (const [key, value] of Object.entries(env)) {
24
+ const line = `${key}="${value}"${
25
+ comment ? " # Added by `npx auth`. Read more: https://cli.authjs.dev" : ""
26
+ }`
27
+ try {
28
+ if (!read) {
29
+ content = await readFile(file, "utf-8")
30
+ read = true
31
+ }
32
+ if (!content.includes(`${key}=`)) {
33
+ console.log(`➕ Added \`${key}\` to ${y.italic(file)}.`)
34
+ content = content ? `${content}\n${line}` : line
35
+ } else {
36
+ const { overwrite } = await prompt({
37
+ type: "confirm",
38
+ name: "overwrite",
39
+ message: `Overwrite existing \`${key}\`?`,
40
+ initial: false,
41
+ })
42
+ if (!overwrite) continue
43
+ console.log(`✨ Updated \`${key}\` in ${y.italic(file)}.`)
44
+ content = content.replace(new RegExp(`${key}=(.*)`), `${line}`)
45
+ }
46
+ } catch (error) {
47
+ if (error.code === "ENOENT") {
48
+ if (!created) {
49
+ console.log(`📝 Created ${y.italic(file)} with \`${key}\`.`)
50
+ created = true
51
+ } else {
52
+ console.log(`➕ Added \`${key}\` to ${y.italic(file)}.`)
53
+ }
54
+ content = content ? `${content}\n${line}` : line
55
+ } else {
56
+ throw error
57
+ }
58
+ }
59
+ }
60
+ if (content) await writeFile(file, content)
61
+ }
package/package.json CHANGED
@@ -1,8 +1,20 @@
1
1
  {
2
2
  "name": "auth",
3
- "description": "The CLI tool by Auth.js to supercharge your authentication workflow.",
3
+ "version": "1.1.0",
4
4
  "homepage": "https://cli.authjs.dev",
5
- "version": "1.0.2",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/nextauthjs/auth-cli.git"
8
+ },
9
+ "description": "The CLI tool by Auth.js to supercharge your authentication workflows.",
10
+ "author": "Balázs Orbán <info@balazsorban.com>",
11
+ "contributors": [
12
+ "Balázs Orbán <info@balazsorban.com>",
13
+ "Nico Domino <yo@ndo.dev>",
14
+ "Lluis Agusti <hi@llu.lu>",
15
+ "Thang Huu Vu <hi@thvu.dev>"
16
+ ],
17
+ "license": "MIT",
6
18
  "type": "module",
7
19
  "bin": {
8
20
  "auth": "index.js"
@@ -10,6 +22,8 @@
10
22
  "files": [
11
23
  "*.d.ts*",
12
24
  "*.js",
25
+ "!scripts",
26
+ "commands",
13
27
  "lib",
14
28
  "src"
15
29
  ],
@@ -17,12 +31,33 @@
17
31
  "authjs",
18
32
  "cli"
19
33
  ],
20
- "author": "Balázs Orbán <info@balazsorban.com>",
21
- "license": "MIT",
22
34
  "dependencies": {
23
- "commander": "11.1.0"
35
+ "@inkeep/ai-api": "0.7.2",
36
+ "@inquirer/prompts": "5.1.0",
37
+ "clipboardy": "4.0.0",
38
+ "commander": "12.1.0",
39
+ "open": "10.1.0",
40
+ "ora": "8.0.1",
41
+ "prompts": "2.4.2",
42
+ "yoctocolors": "2.1.0"
24
43
  },
25
44
  "prettier": {
26
45
  "semi": false
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^20.14.9"
49
+ },
50
+ "engines": {
51
+ "node": ">=18"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/nextauthjs/auth-cli/issues"
55
+ },
56
+ "main": "index.js",
57
+ "directories": {
58
+ "lib": "lib"
59
+ },
60
+ "scripts": {
61
+ "generate-docs": "node ./scripts/generate-docs"
27
62
  }
28
63
  }