auth 1.1.1 → 1.2.1

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
@@ -68,65 +68,72 @@ We have an [OpenCollective](https://opencollective.com/nextauth) for companies a
68
68
  <tr>
69
69
  <td align="center" valign="top">
70
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" />
71
+ <img height="96" src="https://avatars.githubusercontent.com/u/49538330?s=200&v=4" alt="Clerk Logo" />
72
72
  </a><br />
73
73
  <div>Clerk</div>
74
74
  <sub>💵</sub>
75
75
  </td>
76
+ <td align="center" valign="top">
77
+ <a href="https://a0.to/signup/nextauthjs" target="_blank">
78
+ <img height="96" src="https://avatars.githubusercontent.com/u/2824157?v=4" alt="Auth0 Logo" />
79
+ </a><br />
80
+ <div>Auth0</div>
81
+ <sub>💵</sub>
82
+ </td>
76
83
  <td align="center" valign="top">
77
84
  <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" />
85
+ <img height="96" src="https://avatars.githubusercontent.com/u/41974756?s=200&v=4" alt="FusionAuth Logo" />
79
86
  </a><br />
80
87
  <div>FusionAuth</div>
81
88
  <sub>💵</sub>
82
89
  </td>
83
90
  <td align="center" valign="top">
84
91
  <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" />
92
+ <img height="96" src="https://avatars.githubusercontent.com/u/69811361?s=200&v=4" alt="Beyond Identity Logo" />
86
93
  </a><br />
87
94
  <div>Beyond Identity</div>
88
95
  <sub>💵</sub>
89
96
  </td>
90
97
  <td align="center" valign="top">
91
98
  <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" />
99
+ <img height="96" src="https://avatars.githubusercontent.com/u/69983493?s=200&v=4" alt="Stytch Logo" />
93
100
  </a><br />
94
101
  <div>Stytch</div>
95
102
  <sub>💵</sub>
96
103
  </td>
97
104
  <td align="center" valign="top">
98
105
  <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" />
106
+ <img height="96" src="https://avatars.githubusercontent.com/u/17219288?s=200&v=4" alt="Prisma Logo" />
100
107
  </a><br />
101
108
  <div>Prisma</div>
102
109
  <sub>💵</sub>
103
110
  </td>
111
+ </tr>
112
+ <tr>
104
113
  <td align="center" valign="top">
105
114
  <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" />
115
+ <img height="96" src="https://avatars.githubusercontent.com/u/47087496?s=200&v=4" alt="Lowdefy Logo" />
107
116
  </a><br />
108
117
  <div>Lowdefy</div>
109
118
  <sub>💵</sub>
110
119
  </td>
111
120
  <td align="center" valign="top">
112
121
  <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" />
122
+ <img height="96" src="https://avatars.githubusercontent.com/u/97479186?s=200&v=4" alt="Descope Logo" />
114
123
  </a><br />
115
124
  <div>Descope</div>
116
125
  <sub>💵</sub>
117
126
  </td>
118
- </tr>
119
- <tr>
120
127
  <td align="center" valign="top">
121
128
  <a href="https://badass.dev" target="_blank">
122
- <img width="108" src="https://avatars.githubusercontent.com/u/136839242?v=4" alt="Badass Courses Logo" />
129
+ <img height="96" src="https://avatars.githubusercontent.com/u/136839242?v=4" alt="Badass Courses Logo" />
123
130
  </a><br />
124
131
  <div>Badass Courses</div>
125
132
  <sub>💵</sub>
126
133
  </td>
127
134
  <td align="center" valign="top">
128
135
  <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" />
136
+ <img height="96" src="https://avatars.githubusercontent.com/u/50438175?v=4" alt="Encore Logo" />
130
137
  </a><br />
131
138
  <div>Encore</div>
132
139
  <sub>💵</sub>
@@ -139,29 +146,31 @@ We have an [OpenCollective](https://opencollective.com/nextauth) for companies a
139
146
  <sub>💵</sub>
140
147
  </td>
141
148
  <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" />
149
+ <a href="https://www.netlight.com/" target="_blank">
150
+ <img height="96" src="https://avatars.githubusercontent.com/u/1672348?s=200&v=4" alt="Netlight logo" />
144
151
  </a><br />
145
- <div>Checkly</div>
152
+ <div>Netlight</div>
146
153
  <sub>☁️</sub>
147
154
  </td>
155
+ </tr>
156
+ <tr>
148
157
  <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" />
158
+ <a href="https://checklyhq.com" target="_blank">
159
+ <img height="96" src="https://avatars.githubusercontent.com/u/25982255?s=200&v=4" alt="Checkly Logo" />
151
160
  </a><br />
152
- <div>superblog</div>
161
+ <div>Checkly</div>
153
162
  <sub>☁️</sub>
154
163
  </td>
155
164
  <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" />
165
+ <a href="https://superblog.ai/" target="_blank">
166
+ <img height="96" src="https://d33wubrfki0l68.cloudfront.net/cdc4a3833bd878933fcc131655878dbf226ac1c5/10cd6/images/logo_bolt_small.png" alt="superblog Logo" />
158
167
  </a><br />
159
- <div>Netlight</div>
168
+ <div>superblog</div>
160
169
  <sub>☁️</sub>
161
170
  </td>
162
171
  <td align="center" valign="top">
163
172
  <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" />
173
+ <img height="96" src="https://avatars.githubusercontent.com/u/14985020?s=200&v=4" alt="Vercel Logo" />
165
174
  </a><br />
166
175
  <div>Vercel</div>
167
176
  <sub>☁️</sub>
package/commands/add.js CHANGED
@@ -3,12 +3,33 @@
3
3
  import * as y from "yoctocolors"
4
4
  import open from "open"
5
5
  import clipboard from "clipboardy"
6
- import { select, input, password } from "@inquirer/prompts"
6
+ import { select, input, password, number } from "@inquirer/prompts"
7
7
  import { requireFramework } from "../lib/detect.js"
8
8
  import { updateEnvFile } from "../lib/write-env.js"
9
9
  import { providers, frameworks } from "../lib/meta.js"
10
- import { secret } from "./index.js"
11
10
  import { link, markdownToAnsi } from "../lib/markdown.js"
11
+ import { appleGenSecret } from "../lib/apple-gen-secret.js"
12
+
13
+ /**
14
+ * @param {string} label
15
+ * @param {string} [defaultValue]
16
+ */
17
+ async function promptInput(label, defaultValue) {
18
+ return input({
19
+ message: `Paste ${y.magenta(label)}:`,
20
+ validate: (value) => !!value,
21
+ default: defaultValue,
22
+ })
23
+ }
24
+
25
+ /** @param {string} label */
26
+ async function promptPassword(label) {
27
+ return password({
28
+ message: `Paste ${y.magenta(label)}:`,
29
+ mask: true,
30
+ validate: (value) => !!value,
31
+ })
32
+ }
12
33
 
13
34
  const choices = Object.entries(providers)
14
35
  .filter(([, { setupUrl }]) => !!setupUrl)
@@ -17,19 +38,19 @@ const choices = Object.entries(providers)
17
38
  /** @param {string | undefined} providerId */
18
39
  export async function action(providerId) {
19
40
  try {
20
- if (!providerId) {
21
- providerId = await select({
41
+ const pid =
42
+ providerId ??
43
+ (await select({
22
44
  message: "What provider do you want to set up?",
23
45
  choices: choices,
24
- })
25
- }
46
+ }))
26
47
 
27
- const provider = providers[providerId]
48
+ const provider = providers[pid]
28
49
  if (!provider?.setupUrl) {
29
50
  console.error(
30
51
  y.red(
31
52
  `Missing instructions for ${
32
- provider?.name ?? providerId
53
+ provider?.name ?? pid
33
54
  }.\nInstructions are available for: ${y.bold(
34
55
  choices.map((choice) => choice.name).join(", ")
35
56
  )}`
@@ -78,35 +99,48 @@ ${y.bold("Callback URL (copied to clipboard)")}: ${url}`
78
99
 
79
100
  await open(provider.setupUrl)
80
101
 
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
- })
102
+ if (providerId === "apple") {
103
+ const clientId = await promptInput("Client ID")
104
+ const keyId = await promptInput("Key ID")
105
+ const teamId = await promptInput("Team ID")
106
+ const privateKey = await input({
107
+ message: "Path to Private Key",
108
+ validate: (value) => !!value,
109
+ default: "./private-key.p8",
110
+ })
90
111
 
91
- console.log(y.dim(`Updating environment variable file...`))
112
+ const expiresInDays =
113
+ (await number({
114
+ message: "Expires in days (default: 180)",
115
+ required: false,
116
+ default: 180,
117
+ })) ?? 180
92
118
 
93
- const varPrefix = `AUTH_${providerId.toUpperCase()}`
119
+ console.log(y.dim("Updating environment variable file..."))
94
120
 
95
- await updateEnvFile({
96
- [`${varPrefix}_ID`]: clientId,
97
- [`${varPrefix}_SECRET`]: clientSecret,
98
- })
121
+ await updateEnvFile({ AUTH_APPLE_ID: clientId })
99
122
 
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
- )
123
+ const secret = await appleGenSecret({
124
+ teamId,
125
+ clientId,
126
+ keyId,
127
+ privateKey,
128
+ expiresInDays,
129
+ })
130
+
131
+ await updateEnvFile({ AUTH_APPLE_SECRET: secret })
132
+ } else {
133
+ const clientId = await promptInput("Client ID")
134
+ const clientSecret = await promptPassword("Client Secret")
108
135
 
109
- await secret.action({})
136
+ console.log(y.dim("Updating environment variable file..."))
137
+
138
+ const varPrefix = `AUTH_${pid.toUpperCase()}`
139
+ await updateEnvFile({
140
+ [`${varPrefix}_ID`]: clientId,
141
+ [`${varPrefix}_SECRET`]: clientSecret,
142
+ })
143
+ }
110
144
 
111
145
  console.log("\n🎉 Done! You can now use this provider in your app.")
112
146
  } catch (error) {
@@ -48,7 +48,7 @@ export async function action(options) {
48
48
 
49
49
  try {
50
50
  await requireFramework(options.path)
51
- await updateEnvFile({ [key]: value }, options.path, true)
51
+ await updateEnvFile({ [key]: value }, options.path)
52
52
  } catch (error) {
53
53
  console.error(y.red(error))
54
54
  }
@@ -0,0 +1,101 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import * as y from "yoctocolors"
4
+
5
+ /**
6
+ * Generates an Apple client secret.
7
+ *
8
+ * @param {object} options
9
+ * @param {string} options.teamId - Apple Team ID.
10
+ * @param {string} options.clientId - Apple Client ID.
11
+ * @param {string} options.keyId - Apple Key ID.
12
+ * @param {string} options.privateKey - Apple Private Key.
13
+ * @param {number} options.expiresInDays - Days until the secret expires.
14
+ *
15
+ * @see https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret
16
+ */
17
+ export async function appleGenSecret({
18
+ teamId: iss,
19
+ clientId: sub,
20
+ keyId: kid,
21
+ privateKey,
22
+ expiresInDays,
23
+ }) {
24
+ const expiresIn = 86400 * expiresInDays
25
+ const exp = Math.ceil(Date.now() / 1000) + expiresIn
26
+
27
+ const secret = await signJWT(sub, iss, kid, privateKey, exp)
28
+
29
+ console.log(
30
+ y.green(
31
+ `Apple client secret generated. Valid until: ${new Date(exp * 1000)}`
32
+ )
33
+ )
34
+
35
+ return secret
36
+ }
37
+
38
+ /**
39
+ *
40
+ * @param {string} sub - Apple client ID.
41
+ * @param {string} iss - Apple team ID.
42
+ * @param {string} kid - Apple key ID.
43
+ * @param {string} privateKeyPath - Apple private key.
44
+ * @param {Date} exp - Expiry date.
45
+ */
46
+ async function signJWT(sub, iss, kid, privateKeyPath, exp) {
47
+ const header = { alg: "ES256", kid }
48
+
49
+ const payload = {
50
+ iss,
51
+ iat: Date.now() / 1000,
52
+ exp,
53
+ aud: "https://appleid.apple.com",
54
+ sub,
55
+ }
56
+
57
+ const parts = [
58
+ toBase64Url(encoder.encode(JSON.stringify(header))),
59
+ toBase64Url(encoder.encode(JSON.stringify(payload))),
60
+ ]
61
+
62
+ const privateKey = fs.readFileSync(path.resolve(privateKeyPath), "utf8")
63
+
64
+ const signature = await sign(parts.join("."), privateKey)
65
+
66
+ parts.push(toBase64Url(signature))
67
+ return parts.join(".")
68
+ }
69
+
70
+ const encoder = new TextEncoder()
71
+ function toBase64Url(data) {
72
+ return btoa(String.fromCharCode(...new Uint8Array(data)))
73
+ .replace(/\+/g, "-")
74
+ .replace(/\//g, "_")
75
+ .replace(/=+$/, "")
76
+ }
77
+
78
+ async function sign(data, private_key) {
79
+ const pem = private_key.replace(
80
+ /-----BEGIN PRIVATE KEY-----|\n|-----END PRIVATE KEY-----/g,
81
+ ""
82
+ )
83
+ const binaryDerString = atob(pem)
84
+ const binaryDer = new Uint8Array(
85
+ [...binaryDerString].map((char) => char.charCodeAt(0))
86
+ )
87
+
88
+ const privateKey = await globalThis.crypto.subtle.importKey(
89
+ "pkcs8",
90
+ binaryDer.buffer,
91
+ { name: "ECDSA", namedCurve: "P-256" },
92
+ true,
93
+ ["sign"]
94
+ )
95
+
96
+ return await globalThis.crypto.subtle.sign(
97
+ { name: "ECDSA", hash: { name: "SHA-256" } },
98
+ privateKey,
99
+ encoder.encode(data)
100
+ )
101
+ }
package/lib/meta.js CHANGED
@@ -56,7 +56,19 @@ export const providers = {
56
56
  coinbase: { name: "Coinbase", setupUrl: undefined },
57
57
  credentials: { name: "Credentials", setupUrl: undefined },
58
58
  descope: { name: "Descope", setupUrl: undefined },
59
- discord: { name: "Discord", setupUrl: undefined },
59
+ discord: {
60
+ name: "Discord",
61
+ setupUrl: "https://discord.com/developers/applications",
62
+ instructions: `\
63
+ 1. Click on *New Application*
64
+ 2. Set *Application name* (can be anything)
65
+ 3. Click on *OAuth2* in the side menu
66
+ 4. Click on *Add Redirect* and paste the callback URI (on your clipboard)
67
+ 5. Click on *Save Changes*
68
+ 6. Copy and paste the *Client ID*
69
+ 7. Copy and paste the *Client Secret*
70
+ `,
71
+ },
60
72
  dribbble: { name: "Dribbble", setupUrl: undefined },
61
73
  dropbox: { name: "Dropbox", setupUrl: undefined },
62
74
  "duende-identity-server6": {
@@ -98,7 +110,21 @@ export const providers = {
98
110
  kakao: { name: "Kakao", setupUrl: undefined },
99
111
  keycloak: { name: "Keycloak", setupUrl: undefined },
100
112
  line: { name: "Line", setupUrl: undefined },
101
- linkedin: { name: "LinkedIn", setupUrl: undefined },
113
+ linkedin: {
114
+ name: "LinkedIn",
115
+ setupUrl: "https://linkedin.com/developers/apps",
116
+ instructions: `\
117
+ 1. Click on *Create app*
118
+ 2. Set *App name* (can be anything)
119
+ 3. Create a *LinkedIn page*
120
+ 4. Set *LinkedIn Page URL* (LinkedIn page you created)
121
+ 5. Set *App logo* (can be anything)
122
+ 6. Navigate to *Auth* in the top menu
123
+ 7. Set *Authorized redirect URLs for your app* (paste the callback URI on your clipboard)
124
+ 8. Copy and paste the *Client ID*
125
+ 9. Copy and paste the *Primary Client Secret*
126
+ `,
127
+ },
102
128
  mailchimp: { name: "Mailchimp", setupUrl: undefined },
103
129
  mailru: { name: "Mail.ru", setupUrl: undefined },
104
130
  mastodon: { name: "Mastodon", setupUrl: undefined },
package/lib/write-env.js CHANGED
@@ -13,7 +13,7 @@ import { detectFramework } from "./detect.js"
13
13
  * @param {string|undefined} envPath
14
14
  * @param {boolean} comment
15
15
  */
16
- export async function updateEnvFile(env, envPath = "", comment = false) {
16
+ export async function updateEnvFile(env, envPath = "", comment = true) {
17
17
  const framework = await detectFramework(envPath)
18
18
  const dotEnvFile = frameworks[framework]?.envFile
19
19
  const file = join(process.cwd(), envPath, dotEnvFile)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auth",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "homepage": "https://cli.authjs.dev",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,20 +32,21 @@
32
32
  "cli"
33
33
  ],
34
34
  "dependencies": {
35
- "@inkeep/ai-api": "0.7.2",
35
+ "@inkeep/ai-api": "0.8.0",
36
36
  "@inquirer/prompts": "5.1.0",
37
37
  "clipboardy": "4.0.0",
38
38
  "commander": "12.1.0",
39
39
  "open": "10.1.0",
40
- "ora": "8.0.1",
40
+ "ora": "8.1.0",
41
41
  "prompts": "2.4.2",
42
- "yoctocolors": "2.1.0"
42
+ "yoctocolors": "2.1.1",
43
+ "zod": "3.23.8"
43
44
  },
44
45
  "prettier": {
45
46
  "semi": false
46
47
  },
47
48
  "devDependencies": {
48
- "@types/node": "^20.14.9"
49
+ "@types/node": "18"
49
50
  },
50
51
  "engines": {
51
52
  "node": ">=18"