auth 1.0.1 → 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 +165 -11
- package/commands/add.js +117 -0
- package/commands/ask.js +90 -0
- package/commands/index.js +4 -0
- package/commands/init.js +121 -0
- package/commands/secret.js +55 -0
- package/index.js +44 -59
- package/lib/detect.js +56 -0
- package/lib/is-online.js +46 -0
- package/lib/markdown.js +59 -0
- package/lib/meta.js +173 -0
- package/lib/pkg-manager.js +70 -0
- package/lib/write-env.js +61 -0
- package/package.json +40 -5
package/README.md
CHANGED
|
@@ -1,32 +1,186 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
<!--
|
|
33
|
+
<!-- GENERATED START -->
|
|
16
34
|
|
|
17
|
-
```
|
|
35
|
+
```
|
|
18
36
|
Usage: auth [options] [command]
|
|
19
37
|
|
|
20
38
|
Options:
|
|
21
|
-
-V, --version
|
|
22
|
-
-h, --help
|
|
39
|
+
-V, --version output the version number
|
|
40
|
+
-h, --help display help for command
|
|
23
41
|
|
|
24
42
|
Commands:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
package/commands/add.js
ADDED
|
@@ -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
|
+
}
|
package/commands/ask.js
ADDED
|
@@ -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
|
+
}
|
package/commands/init.js
ADDED
|
@@ -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,78 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
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
|
+
|
|
3
9
|
// import pkg from "./package.json" assert { type: "json" }
|
|
4
10
|
|
|
5
|
-
import fs from "fs/promises"
|
|
6
|
-
import { join } from "path"
|
|
7
|
-
import { fileURLToPath } from "url"
|
|
11
|
+
import fs from "node:fs/promises"
|
|
12
|
+
import { join } from "node:path"
|
|
13
|
+
import { fileURLToPath } from "node:url"
|
|
14
|
+
|
|
8
15
|
const __dirname = fileURLToPath(new URL(".", import.meta.url))
|
|
9
|
-
const pkg = JSON.parse(
|
|
16
|
+
const pkg = JSON.parse(
|
|
17
|
+
await fs.readFile(join(__dirname, "./package.json"), "utf-8")
|
|
18
|
+
)
|
|
10
19
|
const { name, description, version } = pkg
|
|
11
20
|
|
|
12
21
|
try {
|
|
13
22
|
// TODO: Remove when Node.js 18 is not maintained anymore
|
|
23
|
+
// @ts-expect-error
|
|
14
24
|
globalThis.crypto ??= (await import("crypto")).webcrypto
|
|
15
25
|
} catch {}
|
|
16
26
|
|
|
17
|
-
/** Web compatible method to create a random string of a given length */
|
|
18
|
-
export function randomString(size = 32) {
|
|
19
|
-
const bytes = crypto.getRandomValues(new Uint8Array(size))
|
|
20
|
-
return Buffer.from(bytes, "base64").toString("base64")
|
|
21
|
-
}
|
|
22
|
-
|
|
23
27
|
const program = new Command()
|
|
24
28
|
|
|
25
|
-
program
|
|
29
|
+
program
|
|
30
|
+
.name(name)
|
|
31
|
+
.description(description.replace("Auth.js", y.magenta(y.bold("Auth.js"))))
|
|
32
|
+
.version(version)
|
|
26
33
|
|
|
27
34
|
program
|
|
28
|
-
.command("
|
|
29
|
-
.option("--
|
|
30
|
-
.
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
if (options.raw) return console.log(value)
|
|
34
|
-
// TODO: Detect framework, check for existing value, and write automatically
|
|
35
|
-
console.log(`
|
|
36
|
-
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)
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
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)
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
sveltekit: {
|
|
48
|
-
src: "https://github.com/nextauthjs/sveltekit-auth-example",
|
|
49
|
-
demo: "https://sveltekit-auth-example.vercel.app",
|
|
50
|
-
},
|
|
51
|
-
express: {
|
|
52
|
-
src: "https://github.com/nextauthjs/express-auth-example",
|
|
53
|
-
demo: "https://express-auth-example.vercel.app",
|
|
54
|
-
},
|
|
55
|
-
}
|
|
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)
|
|
56
54
|
|
|
57
55
|
program
|
|
58
|
-
.command("
|
|
59
|
-
.argument("[
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
})
|
|
66
|
-
.description("Clone a framework template.")
|
|
67
|
-
.action((framework) => {
|
|
68
|
-
if (!framework) {
|
|
69
|
-
return console.log(`
|
|
70
|
-
Supported frameworks are: ${Object.keys(frameworks).join(", ")}`)
|
|
71
|
-
}
|
|
72
|
-
const { src, demo } = frameworks[framework]
|
|
73
|
-
console.log(`
|
|
74
|
-
Source code: ${src}
|
|
75
|
-
Deployed demo: ${demo}`)
|
|
76
|
-
})
|
|
56
|
+
.command("add")
|
|
57
|
+
.argument("[provider]", "The authentication provider.")
|
|
58
|
+
.description('Register a new authentication provider')
|
|
59
|
+
.action(add.action)
|
|
60
|
+
|
|
61
|
+
program.parse()
|
|
77
62
|
|
|
78
|
-
program
|
|
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
|
+
|
package/lib/is-online.js
ADDED
|
@@ -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
|
+
}
|
package/lib/markdown.js
ADDED
|
@@ -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
|
+
}
|
package/lib/write-env.js
ADDED
|
@@ -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
|
-
"
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"homepage": "https://cli.authjs.dev",
|
|
5
|
-
"
|
|
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
|
-
"
|
|
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
|
}
|