create-vexcms 0.1.0-alpha.2 → 0.1.0-alpha.21

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.
Files changed (115) hide show
  1. package/README.md +48 -14
  2. package/dist/index.js +1714 -1504
  3. package/package.json +2 -2
  4. package/templates/base-nextjs/README.md +18 -5
  5. package/templates/base-nextjs/convex/auth/index.ts +2 -2
  6. package/templates/base-nextjs/convex/auth/options.ts +27 -26
  7. package/templates/base-nextjs/convex/auth/schema.ts +51 -0
  8. package/templates/base-nextjs/convex/auth/sessions.ts +3 -3
  9. package/templates/base-nextjs/convex/tsconfig.json +2 -2
  10. package/templates/base-nextjs/convex/vex/firstUser.ts +2 -2
  11. package/templates/base-nextjs/convex/vex/globals.ts +3 -2
  12. package/templates/base-nextjs/convex/vex/media.ts +3 -2
  13. package/templates/base-nextjs/convex/vex.schema.ts +1 -0
  14. package/templates/base-nextjs/convex/vex.ts +17 -4
  15. package/templates/base-nextjs/next.config.ts +52 -6
  16. package/templates/base-nextjs/package.json +18 -7
  17. package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
  18. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +27 -15
  19. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +2 -2
  20. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +9 -14
  21. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +8 -6
  22. package/templates/base-nextjs/src/app/api/vex/revalidate/route.ts +19 -0
  23. package/templates/base-nextjs/src/app/globals.css +10 -9
  24. package/templates/base-nextjs/src/app/layout.tsx +6 -0
  25. package/templates/base-nextjs/src/auth/access.ts +14 -2
  26. package/templates/base-nextjs/src/auth/serverUtils.ts +20 -6
  27. package/templates/base-nextjs/src/components/WelcomePage.tsx +54 -43
  28. package/templates/base-nextjs/src/components/component-example.tsx +1 -1
  29. package/templates/base-nextjs/src/components/providers/server.tsx +16 -8
  30. package/templates/base-nextjs/src/vex.config.server.ts +30 -0
  31. package/templates/base-nextjs/src/vex.config.ts +14 -11
  32. package/templates/base-nextjs/src/vex.types.ts +2 -1
  33. package/templates/base-nextjs/src/vexcms/collections/images.ts +1 -1
  34. package/templates/base-nextjs/src/vexcms/collections/users.ts +3 -1
  35. package/templates/marketing-site/convex/pages.ts +20 -1
  36. package/templates/marketing-site/convex/schema.ts +4 -4
  37. package/templates/marketing-site/convex/seed.ts +515 -70
  38. package/templates/marketing-site/convex/siteSettings.ts +1 -1
  39. package/templates/marketing-site/convex/theme.ts +3 -3
  40. package/templates/marketing-site/convex/vex.schema.ts +75 -38
  41. package/templates/marketing-site/public/favicon.svg +14 -0
  42. package/templates/marketing-site/public/favicons/apple-touch-icon.png +0 -0
  43. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  44. package/templates/marketing-site/src/app/(frontend)/(site)/PageContent.tsx +64 -0
  45. package/templates/marketing-site/src/app/(frontend)/(site)/SiteLivePreviewProvider.tsx +31 -0
  46. package/templates/marketing-site/src/app/(frontend)/(site)/[slug]/page.tsx +63 -0
  47. package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +50 -0
  48. package/templates/marketing-site/src/app/(frontend)/(site)/page.tsx +33 -0
  49. package/templates/marketing-site/src/app/(frontend)/layout.tsx +10 -24
  50. package/templates/marketing-site/src/app/(vexcms)/admin/layout.tsx +32 -0
  51. package/templates/marketing-site/src/app/globals.css +413 -0
  52. package/templates/marketing-site/src/app/layout.tsx +85 -0
  53. package/templates/marketing-site/src/app/robots.ts +13 -0
  54. package/templates/marketing-site/src/app/sitemap.ts +18 -0
  55. package/templates/marketing-site/src/components/BrandMark.tsx +27 -0
  56. package/templates/marketing-site/src/components/CodeHighlightContext.tsx +76 -0
  57. package/templates/marketing-site/src/components/CodePane.tsx +98 -0
  58. package/templates/marketing-site/src/components/Container.tsx +25 -0
  59. package/templates/marketing-site/src/components/CopyButton.tsx +80 -0
  60. package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +54 -0
  61. package/templates/marketing-site/src/components/InstallCommand.tsx +29 -0
  62. package/templates/marketing-site/src/components/MediaImage.tsx +59 -0
  63. package/templates/marketing-site/src/components/SectionHeader.tsx +51 -0
  64. package/templates/marketing-site/src/components/SiteFooter.tsx +8 -6
  65. package/templates/marketing-site/src/components/SiteHeader.tsx +8 -6
  66. package/templates/marketing-site/src/components/ThemeLive.tsx +1 -2
  67. package/templates/marketing-site/src/components/ThemeStyle.tsx +3 -3
  68. package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +44 -120
  69. package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
  70. package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +47 -266
  71. package/templates/marketing-site/src/lib/codeHighlightKey.ts +27 -0
  72. package/templates/marketing-site/src/lib/highlight.ts +134 -0
  73. package/templates/marketing-site/src/lib/highlightClient.ts +37 -0
  74. package/templates/marketing-site/src/lib/metadata.ts +61 -33
  75. package/templates/marketing-site/src/lib/resolvePagePath.ts +8 -0
  76. package/templates/marketing-site/src/lib/vex.ts +18 -0
  77. package/templates/marketing-site/src/proxy.ts +179 -0
  78. package/templates/marketing-site/src/vex.config.server.ts +27 -0
  79. package/templates/marketing-site/src/vex.config.ts +27 -9
  80. package/templates/marketing-site/src/vex.types.ts +102 -76
  81. package/templates/marketing-site/src/vexcms/api.ts +2 -2
  82. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -2
  83. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +40 -24
  84. package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/config.ts +62 -0
  85. package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/index.tsx +106 -0
  86. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -3
  87. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +63 -50
  88. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +1 -0
  89. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +43 -58
  90. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +6 -3
  91. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +90 -53
  92. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +10 -3
  93. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +205 -138
  94. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +27 -12
  95. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +174 -119
  96. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +2 -1
  97. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +59 -51
  98. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +26 -5
  99. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +256 -73
  100. package/templates/marketing-site/src/vexcms/blocks/Split/config.ts +62 -0
  101. package/templates/marketing-site/src/vexcms/blocks/Split/index.tsx +186 -0
  102. package/templates/marketing-site/src/vexcms/blocks/Stats/config.ts +35 -0
  103. package/templates/marketing-site/src/vexcms/blocks/Stats/index.tsx +77 -0
  104. package/templates/marketing-site/src/vexcms/blocks/config.ts +27 -11
  105. package/templates/marketing-site/src/vexcms/blocks/constants.ts +3 -0
  106. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -0
  107. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -0
  108. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -0
  109. package/templates/marketing-site/src/vexcms/collections/pages.ts +23 -1
  110. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +14 -14
  111. package/templates/marketing-site/src/vexcms/collections/users.ts +20 -0
  112. package/dist/index.mjs +0 -30976
  113. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +0 -53
  114. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +0 -25
  115. package/templates/marketing-site/src/app/(frontend)/page.tsx +0 -21
package/dist/index.js CHANGED
@@ -121,7 +121,7 @@ function rewriteManifestForMonorepo(props) {
121
121
  }
122
122
 
123
123
  // src/installers/base.ts
124
- import fs4 from "fs-extra";
124
+ import fs5 from "fs-extra";
125
125
  import { execa } from "execa";
126
126
  import ora from "ora";
127
127
  import crypto from "crypto";
@@ -173,2167 +173,2377 @@ async function overlayTemplate({ overlayDir, targetDir }) {
173
173
  await fs3.copy(overlayDir, targetDir, { overwrite: true });
174
174
  }
175
175
 
176
- // src/installers/base.ts
177
- var VexFrameworkInstaller = class {
178
- /**
179
- * Binds this installer to the destination directory and package name used
180
- * throughout scaffolding (template copy target, package.json, README, etc).
181
- *
182
- * @param targetPath - Absolute path of the directory the project is created in.
183
- * @param projectName - npm package name recorded for subclasses; not read directly by the base class.
184
- */
185
- constructor(targetPath, projectName) {
186
- this.targetPath = targetPath;
187
- this.projectName = projectName;
188
- }
189
- targetPath;
190
- projectName;
191
- /**
192
- * Copy base template files to target directory
193
- */
194
- async copyBaseFiles() {
195
- await copyTemplate(this.frameworkName, this.targetPath);
196
- }
197
- /**
198
- * Apply a template overlay on top of the base template.
199
- * Merges overlay files onto the already-copied base — existing files
200
- * not in the overlay are left untouched.
201
- *
202
- * @param overlay - Name of the overlay template directory under `templates/` (e.g. `"marketing-site"`).
203
- */
204
- async applyTemplateOverlay(overlay) {
205
- const { dirname: dirname2 } = await import("path");
206
- const { fileURLToPath: fileURLToPath2 } = await import("url");
207
- const __filename = fileURLToPath2(import.meta.url);
208
- const __dirname = dirname2(__filename);
209
- let overlayDir = path2.join(__dirname, "../../templates", overlay);
210
- if (!await fs4.pathExists(overlayDir)) {
211
- overlayDir = path2.join(__dirname, "../templates", overlay);
176
+ // src/installers/string-utils.ts
177
+ import fs4 from "fs-extra";
178
+
179
+ // src/installers/providers.ts
180
+ var OAUTH_PROVIDERS = {
181
+ // ========================================
182
+ // POPULAR PROVIDERS (10)
183
+ // ========================================
184
+ google: {
185
+ id: "google",
186
+ name: "Google",
187
+ envPrefix: "GOOGLE",
188
+ clientIdVar: "GOOGLE_CLIENT_ID",
189
+ clientSecretVar: "GOOGLE_CLIENT_SECRET",
190
+ popular: true,
191
+ betterAuthConfig: {
192
+ import: "",
193
+ clientSideProvider: '"google"',
194
+ socialProvider: `google({
195
+ clientId: process.env.GOOGLE_CLIENT_ID!,
196
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
197
+ })`,
198
+ scopes: []
199
+ },
200
+ env: [
201
+ {
202
+ name: "GOOGLE_CLIENT_ID",
203
+ type: "server",
204
+ description: "Google OAuth Client ID"
205
+ },
206
+ {
207
+ name: "GOOGLE_CLIENT_SECRET",
208
+ type: "server",
209
+ description: "Google OAuth Client Secret"
210
+ }
211
+ ],
212
+ docs: {
213
+ provider: "https://console.cloud.google.com/apis/credentials",
214
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
215
+ },
216
+ requiresExtraConfig: false,
217
+ extraConfigNotes: "",
218
+ readme: {
219
+ title: "Google OAuth Setup",
220
+ content: `## Google OAuth Setup
221
+
222
+ 1. Create OAuth credentials at https://console.cloud.google.com/apis/credentials
223
+ 2. Set the Authorized redirect URI to: \`http://localhost:3000/api/auth/callback/google\` (update for production)
224
+ 3. Copy the Client ID and Client Secret to your \`.env\` file
225
+
226
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
212
227
  }
213
- await overlayTemplate({ overlayDir, targetDir: this.targetPath });
214
- }
215
- /**
216
- * Update the package.json name field, the vex:update script (to match the
217
- * detected package manager), and README.md's `{{PROJECT_NAME}}` heading.
218
- *
219
- * @param name - npm package name to write into `package.json`'s `name` field
220
- * and to substitute for `{{PROJECT_NAME}}` in `README.md`.
221
- */
222
- async updatePackageName(name) {
223
- const pkgPath = path2.join(this.targetPath, "package.json");
224
- const pkg = await fs4.readJson(pkgPath);
225
- pkg.name = name;
226
- const pm = this.detectPackageManager();
227
- if (pkg.scripts?.["vex:update"] && pm !== "pnpm") {
228
- const addCmd = pm === "yarn" ? "yarn add" : pm === "bun" ? "bun add" : "npm install";
229
- pkg.scripts["vex:update"] = pkg.scripts["vex:update"].replace(/^pnpm add/, addCmd);
228
+ },
229
+ github: {
230
+ id: "github",
231
+ name: "GitHub",
232
+ envPrefix: "GITHUB",
233
+ clientIdVar: "GITHUB_CLIENT_ID",
234
+ clientSecretVar: "GITHUB_CLIENT_SECRET",
235
+ popular: true,
236
+ betterAuthConfig: {
237
+ import: "",
238
+ clientSideProvider: '"github"',
239
+ socialProvider: `github({
240
+ clientId: process.env.GITHUB_CLIENT_ID!,
241
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
242
+ })`,
243
+ scopes: ["user:email"]
244
+ },
245
+ env: [
246
+ {
247
+ name: "GITHUB_CLIENT_ID",
248
+ type: "server",
249
+ description: "GitHub OAuth App Client ID"
250
+ },
251
+ {
252
+ name: "GITHUB_CLIENT_SECRET",
253
+ type: "server",
254
+ description: "GitHub OAuth App Client Secret"
255
+ }
256
+ ],
257
+ docs: {
258
+ provider: "https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app",
259
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
260
+ },
261
+ requiresExtraConfig: true,
262
+ extraConfigNotes: "You MUST include the user:email scope in your GitHub app. For GitHub Apps, enable Read-Only access to Email Addresses in Permissions.",
263
+ readme: {
264
+ title: "GitHub OAuth Setup",
265
+ content: `## GitHub OAuth Setup
266
+
267
+ 1. Create a GitHub OAuth App at https://github.com/settings/developers
268
+ 2. Set the Authorization callback URL to: \`http://localhost:3000/api/auth/callback/github\` (update for production)
269
+ 3. Copy the Client ID and Client Secret to your \`.env\` file
270
+ 4. **Important**: Include the \`user:email\` scope in your GitHub app permissions
271
+
272
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
230
273
  }
231
- await fs4.writeJson(pkgPath, pkg, { spaces: 2 });
232
- const readmePath = path2.join(this.targetPath, "README.md");
233
- if (await fs4.pathExists(readmePath)) {
234
- const readme = await fs4.readFile(readmePath, "utf-8");
235
- await fs4.writeFile(readmePath, readme.replace("{{PROJECT_NAME}}", name));
274
+ },
275
+ discord: {
276
+ id: "discord",
277
+ name: "Discord",
278
+ envPrefix: "DISCORD",
279
+ clientIdVar: "DISCORD_CLIENT_ID",
280
+ clientSecretVar: "DISCORD_CLIENT_SECRET",
281
+ popular: true,
282
+ betterAuthConfig: {
283
+ import: "",
284
+ clientSideProvider: '"discord"',
285
+ socialProvider: `discord({
286
+ clientId: process.env.DISCORD_CLIENT_ID!,
287
+ clientSecret: process.env.DISCORD_CLIENT_SECRET!,
288
+ })`,
289
+ scopes: []
290
+ },
291
+ env: [
292
+ {
293
+ name: "DISCORD_CLIENT_ID",
294
+ type: "server",
295
+ description: "Discord OAuth Application Client ID"
296
+ },
297
+ {
298
+ name: "DISCORD_CLIENT_SECRET",
299
+ type: "server",
300
+ description: "Discord OAuth Application Client Secret"
301
+ }
302
+ ],
303
+ docs: {
304
+ provider: "https://discord.com/developers/applications",
305
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
306
+ },
307
+ requiresExtraConfig: false,
308
+ extraConfigNotes: "You can optionally add a permissions field to request additional Discord permissions.",
309
+ readme: {
310
+ title: "Discord OAuth Setup",
311
+ content: `## Discord OAuth Setup
312
+
313
+ 1. Create an application at https://discord.com/developers/applications
314
+ 2. Add a redirect URL: \`http://localhost:3000/api/auth/callback/discord\` (update for production)
315
+ 3. Copy the Client ID and Client Secret to your \`.env\` file
316
+
317
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
236
318
  }
237
- }
238
- /**
239
- * Configure the dev server port.
240
- * Updates package.json dev script and creates .env.local with NEXT_PUBLIC_SITE_URL.
241
- *
242
- * @param port - Dev server port number to write into the `dev` script and `.env.local`.
243
- */
244
- async configurePort(port) {
245
- const pkgPath = path2.join(this.targetPath, "package.json");
246
- const pkg = await fs4.readJson(pkgPath);
247
- if (pkg.scripts?.dev) {
248
- if (pkg.scripts.dev.includes("--port")) {
249
- pkg.scripts.dev = pkg.scripts.dev.replace(/--port[= ]\d+/, `--port=${port}`);
250
- } else {
251
- pkg.scripts.dev = `${pkg.scripts.dev} --port=${port}`;
319
+ },
320
+ apple: {
321
+ id: "apple",
322
+ name: "Apple",
323
+ envPrefix: "APPLE",
324
+ clientIdVar: "APPLE_CLIENT_ID",
325
+ clientSecretVar: "APPLE_CLIENT_SECRET",
326
+ popular: true,
327
+ betterAuthConfig: {
328
+ import: "",
329
+ clientSideProvider: '"apple"',
330
+ socialProvider: `apple({
331
+ clientId: process.env.APPLE_CLIENT_ID!,
332
+ clientSecret: process.env.APPLE_CLIENT_SECRET!,
333
+ })`,
334
+ scopes: []
335
+ },
336
+ env: [
337
+ {
338
+ name: "APPLE_CLIENT_ID",
339
+ type: "server",
340
+ description: "Apple Sign In Service ID"
341
+ },
342
+ {
343
+ name: "APPLE_CLIENT_SECRET",
344
+ type: "server",
345
+ description: "Apple Sign In Client Secret (JWT token)"
252
346
  }
347
+ ],
348
+ docs: {
349
+ provider: "https://developer.apple.com/sign-in-with-apple/get-started/",
350
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
351
+ },
352
+ requiresExtraConfig: true,
353
+ extraConfigNotes: "Apple requires additional configuration: clientId is your Service ID, clientSecret must be a JWT token generated from your Team ID, Key ID, and Private Key. You may also need to provide appBundleIdentifier for iOS apps.",
354
+ readme: {
355
+ title: "Apple Sign In Setup",
356
+ content: `## Apple Sign In Setup
357
+
358
+ 1. Create a Sign In with Apple service at https://developer.apple.com
359
+ 2. Configure your Service ID and return URLs
360
+ 3. Generate a client secret JWT using your private key, Team ID, and Key ID
361
+ 4. Add credentials to your \`.env\` file
362
+
363
+ **Note**: The clientSecret must be a JWT token generated from your Apple credentials, not a standard client secret.
364
+
365
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
253
366
  }
254
- await fs4.writeJson(pkgPath, pkg, { spaces: 2 });
255
- const siteUrl = `http://localhost:${port}`;
256
- const envLocalPath = path2.join(this.targetPath, ".env.local");
257
- const envContent = [
258
- `NEXT_PUBLIC_SITE_URL=${siteUrl}`,
259
- `SITE_URL=${siteUrl}`,
260
- `BETTER_AUTH_SECRET=${this.generateAuthSecret()}`,
261
- `NEXT_PUBLIC_CONVEX_URL=https://placeholder.convex.cloud`,
262
- `NEXT_PUBLIC_CONVEX_SITE_URL=https://placeholder.convex.site`,
263
- `CONVEX_DEPLOYMENT=`,
264
- ""
265
- ].join("\n");
266
- await fs4.writeFile(envLocalPath, envContent);
267
- }
268
- /**
269
- * Rewrites the scaffolded `package.json` for life inside a host pnpm
270
- * workspace (`--monorepo`). `@vexcms/*` dependencies become
271
- * `workspace:*`; any other dependency the host's `pnpm-workspace.yaml`
272
- * catalog also pins becomes `catalog:`; everything else keeps its
273
- * literal, catalog-resolved version. No-op when `options.monorepo` is
274
- * false or `options.workspaceRoot` is unset.
275
- *
276
- * @param options - Resolved project options; only `monorepo`/`workspaceRoot` are consulted.
277
- */
278
- async applyMonorepoRewrite(options) {
279
- if (!options.monorepo || !options.workspaceRoot) return;
280
- const pkgPath = path2.join(this.targetPath, "package.json");
281
- const manifest = await fs4.readJson(pkgPath);
282
- const catalog = await readWorkspaceCatalog({ workspaceRoot: options.workspaceRoot });
283
- const rewritten = rewriteManifestForMonorepo({ manifest, catalog });
284
- await fs4.writeJson(pkgPath, rewritten, { spaces: 2 });
285
- }
286
- /**
287
- * Detect the package manager used to invoke the CLI
288
- *
289
- * @returns The package manager inferred from `npm_config_user_agent`, defaulting to `"npm"` when it can't be determined.
290
- */
291
- detectPackageManager() {
292
- const userAgent = process.env.npm_config_user_agent;
293
- if (userAgent) {
294
- if (userAgent.includes("pnpm")) return "pnpm";
295
- if (userAgent.includes("yarn")) return "yarn";
296
- if (userAgent.includes("bun")) return "bun";
297
- }
298
- return "npm";
299
- }
300
- /**
301
- * Install project dependencies using detected package manager
302
- */
303
- async installDependencies() {
304
- const packageManager = this.detectPackageManager();
305
- const spinner = ora(`Installing dependencies with ${packageManager}...`).start();
306
- try {
307
- await execa(packageManager, ["install"], {
308
- cwd: this.targetPath,
309
- stdio: "pipe"
310
- });
311
- spinner.succeed("Dependencies installed successfully");
312
- } catch (error) {
313
- spinner.fail("Failed to install dependencies");
314
- throw new Error(
315
- `Dependency installation failed: ${error instanceof Error ? error.message : "Unknown error"}`
316
- );
317
- }
318
- }
319
- /**
320
- * Format code using project's formatter
321
- */
322
- async formatCode() {
323
- const packageManager = this.detectPackageManager();
324
- const spinner = ora("Formatting code...").start();
325
- try {
326
- const result = await execa(packageManager, ["run", "format"], {
327
- cwd: this.targetPath,
328
- stdio: "pipe",
329
- reject: false
330
- });
331
- if (result.exitCode === 0) {
332
- spinner.succeed("Code formatted successfully");
333
- } else {
334
- spinner.warn("Formatting completed with warnings");
367
+ },
368
+ microsoft: {
369
+ id: "microsoft",
370
+ name: "Microsoft",
371
+ envPrefix: "MICROSOFT",
372
+ clientIdVar: "MICROSOFT_CLIENT_ID",
373
+ clientSecretVar: "MICROSOFT_CLIENT_SECRET",
374
+ popular: true,
375
+ betterAuthConfig: {
376
+ import: "",
377
+ clientSideProvider: '"microsoft"',
378
+ socialProvider: `microsoft({
379
+ clientId: process.env.MICROSOFT_CLIENT_ID!,
380
+ clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
381
+ tenantId: process.env.MICROSOFT_TENANT_ID,
382
+ })`,
383
+ scopes: []
384
+ },
385
+ env: [
386
+ {
387
+ name: "MICROSOFT_CLIENT_ID",
388
+ type: "server",
389
+ description: "Microsoft Entra ID Application (client) ID"
390
+ },
391
+ {
392
+ name: "MICROSOFT_CLIENT_SECRET",
393
+ type: "server",
394
+ description: "Microsoft Entra ID Client Secret"
395
+ },
396
+ {
397
+ name: "MICROSOFT_TENANT_ID",
398
+ type: "server",
399
+ description: 'Microsoft Entra ID Tenant ID (optional, defaults to "common")'
335
400
  }
336
- } catch (error) {
337
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
338
- spinner.warn(`Failed to format code: ${errorMessage}`);
401
+ ],
402
+ docs: {
403
+ provider: "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps",
404
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
405
+ },
406
+ requiresExtraConfig: true,
407
+ extraConfigNotes: 'Optional fields: tenantId (defaults to "common"), authority (custom authority URL), prompt (consent behavior).',
408
+ readme: {
409
+ title: "Microsoft Entra ID OAuth Setup",
410
+ content: `## Microsoft Entra ID OAuth Setup
411
+
412
+ 1. Register an application at https://portal.azure.com
413
+ 2. Add a redirect URI: \`http://localhost:3000/api/auth/callback/microsoft\` (update for production)
414
+ 3. Create a client secret in "Certificates & secrets"
415
+ 4. Copy the Application (client) ID and Client Secret to your \`.env\` file
416
+ 5. (Optional) Copy the Directory (tenant) ID if you want to restrict to a specific tenant
417
+
418
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
339
419
  }
340
- }
341
- /**
342
- * Lint and fix code using project's ESLint configuration
343
- */
344
- async lintCode() {
345
- const packageManager = this.detectPackageManager();
346
- const spinner = ora("Linting and fixing code...").start();
347
- try {
348
- const args2 = packageManager === "pnpm" ? ["lint", "--fix"] : ["run", "lint", "--", "--fix"];
349
- const result = await execa(packageManager, args2, {
350
- cwd: this.targetPath,
351
- stdio: "pipe",
352
- reject: false
353
- });
354
- if (result.exitCode === 0) {
355
- spinner.succeed("Code linted and fixed successfully");
356
- } else {
357
- spinner.warn("Linting completed with warnings");
420
+ },
421
+ facebook: {
422
+ id: "facebook",
423
+ name: "Facebook",
424
+ envPrefix: "FACEBOOK",
425
+ clientIdVar: "FACEBOOK_CLIENT_ID",
426
+ clientSecretVar: "FACEBOOK_CLIENT_SECRET",
427
+ popular: true,
428
+ betterAuthConfig: {
429
+ import: "",
430
+ clientSideProvider: '"facebook"',
431
+ socialProvider: `facebook({
432
+ clientId: process.env.FACEBOOK_CLIENT_ID!,
433
+ clientSecret: process.env.FACEBOOK_CLIENT_SECRET!,
434
+ })`,
435
+ scopes: []
436
+ },
437
+ env: [
438
+ {
439
+ name: "FACEBOOK_CLIENT_ID",
440
+ type: "server",
441
+ description: "Facebook App ID"
442
+ },
443
+ {
444
+ name: "FACEBOOK_CLIENT_SECRET",
445
+ type: "server",
446
+ description: "Facebook App Secret"
358
447
  }
359
- } catch (error) {
360
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
361
- spinner.warn(`Failed to lint code: ${errorMessage}`);
448
+ ],
449
+ docs: {
450
+ provider: "https://developers.facebook.com/apps/",
451
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
452
+ },
453
+ requiresExtraConfig: false,
454
+ extraConfigNotes: "Facebook supports custom scopes and fields arrays to request additional user data.",
455
+ readme: {
456
+ title: "Facebook OAuth Setup",
457
+ content: `## Facebook OAuth Setup
458
+
459
+ 1. Create an app at https://developers.facebook.com/apps/
460
+ 2. Add Facebook Login product to your app
461
+ 3. Add OAuth redirect URI: \`http://localhost:3000/api/auth/callback/facebook\` (update for production)
462
+ 4. Copy the App ID and App Secret to your \`.env\` file
463
+
464
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
362
465
  }
363
- }
364
- /**
365
- * Initialize Git repository in target directory
366
- */
367
- async initGitRepo() {
368
- const spinner = ora("Initializing Git repository...").start();
369
- try {
370
- try {
371
- await execa("git", ["--version"], { stdio: "pipe" });
372
- } catch {
373
- spinner.fail("Git is not installed");
374
- throw new Error("Git is not installed. Please install Git to initialize a repository.");
466
+ },
467
+ twitter: {
468
+ id: "twitter",
469
+ name: "Twitter/X",
470
+ envPrefix: "TWITTER",
471
+ clientIdVar: "TWITTER_CLIENT_ID",
472
+ clientSecretVar: "TWITTER_CLIENT_SECRET",
473
+ popular: true,
474
+ betterAuthConfig: {
475
+ import: "",
476
+ clientSideProvider: '"twitter"',
477
+ socialProvider: `twitter({
478
+ clientId: process.env.TWITTER_CLIENT_ID!,
479
+ clientSecret: process.env.TWITTER_CLIENT_SECRET!,
480
+ })`,
481
+ scopes: []
482
+ },
483
+ env: [
484
+ {
485
+ name: "TWITTER_CLIENT_ID",
486
+ type: "server",
487
+ description: "Twitter/X OAuth 2.0 Client ID"
488
+ },
489
+ {
490
+ name: "TWITTER_CLIENT_SECRET",
491
+ type: "server",
492
+ description: "Twitter/X OAuth 2.0 Client Secret"
375
493
  }
376
- await execa("git", ["init"], { cwd: this.targetPath, stdio: "pipe" });
377
- await execa("git", ["add", "."], { cwd: this.targetPath, stdio: "pipe" });
378
- await execa(
379
- "git",
380
- ["commit", "-m", "Initial commit from create-vexcms"],
381
- { cwd: this.targetPath, stdio: "pipe" }
382
- );
383
- spinner.succeed("Git repository initialized");
384
- } catch (error) {
385
- spinner.fail("Failed to initialize Git repository");
386
- throw new Error(
387
- `Git initialization failed: ${error instanceof Error ? error.message : "Unknown error"}`
388
- );
389
- }
390
- }
391
- /**
392
- * Generate a secure random secret for Better Auth
393
- *
394
- * @returns A 64-character hex string (32 random bytes) suitable for `BETTER_AUTH_SECRET`.
395
- */
396
- generateAuthSecret() {
397
- return crypto.randomBytes(32).toString("hex");
398
- }
399
- /**
400
- * Write the generated auth secret to .env.example
401
- */
402
- async writeAuthSecret() {
403
- const envPath = path2.join(this.targetPath, ".env.example");
404
- const secret = this.generateAuthSecret();
405
- if (await fs4.pathExists(envPath)) {
406
- let content = await fs4.readFile(envPath, "utf-8");
407
- content = content.replace(
408
- /BETTER_AUTH_SECRET=.*/,
409
- `BETTER_AUTH_SECRET=${secret}`
410
- );
411
- await fs4.writeFile(envPath, content);
412
- }
413
- }
414
- /**
415
- * Configure the Better Auth organizations plugin.
416
- * When enabled, replaces placeholders with organization import and plugin.
417
- * When disabled, removes the placeholder lines.
418
- *
419
- * @param enabled - Whether the organizations (multi-tenant) plugin should be wired into `convex/auth/plugins/index.ts`.
420
- */
421
- async configureOrganizations(enabled) {
422
- const pluginsPath = path2.join(this.targetPath, "convex/auth/plugins/index.ts");
423
- if (!await fs4.pathExists(pluginsPath)) return;
424
- let content = await fs4.readFile(pluginsPath, "utf-8");
425
- if (enabled) {
426
- content = content.replace(
427
- "// {{ORGANIZATIONS_IMPORT}}",
428
- 'import { organization } from "better-auth/plugins"'
429
- );
430
- content = content.replace(
431
- " // {{ORGANIZATIONS_PLUGIN}}",
432
- " organization(),"
433
- );
434
- } else {
435
- content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_IMPORT\}\}\n?/, "");
436
- content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_PLUGIN\}\}\n?/, "");
437
- }
438
- await fs4.writeFile(pluginsPath, content);
439
- }
440
- /**
441
- * Main orchestration method for project initialization
442
- *
443
- * @param options - Resolved project options from CLI flags and prompts, driving every scaffolding step (template copy, overlay, port, auth, README, git, install).
444
- */
445
- async initProject(options) {
446
- const copySpinner = ora("Copying template files...").start();
447
- try {
448
- await this.copyBaseFiles();
449
- copySpinner.succeed("Template files copied");
450
- } catch (error) {
451
- copySpinner.fail("Failed to copy template files");
452
- throw error;
453
- }
454
- if (!options.bare) {
455
- const overlaySpinner = ora("Applying marketing site template...").start();
456
- try {
457
- await this.applyTemplateOverlay("marketing-site");
458
- overlaySpinner.succeed("Marketing site template applied");
459
- } catch (error) {
460
- overlaySpinner.fail("Failed to apply template overlay");
461
- throw error;
462
- }
463
- }
464
- const nameSpinner = ora("Configuring project...").start();
465
- try {
466
- await this.updatePackageName(options.projectName);
467
- await this.configurePort(options.port);
468
- nameSpinner.succeed("Project configured");
469
- } catch (error) {
470
- nameSpinner.fail("Failed to configure project");
471
- throw error;
472
- }
473
- const authSpinner = ora("Configuring authentication...").start();
474
- try {
475
- await this.updateOAuthConfig(options.oauthProviders, options.emailPasswordAuth);
476
- authSpinner.succeed(
477
- options.emailPasswordAuth || options.oauthProviders.length > 0 ? "Authentication configuration updated" : "Authentication placeholders cleaned up"
478
- );
479
- } catch (error) {
480
- authSpinner.fail("Failed to configure authentication");
481
- throw error;
482
- }
483
- const oauthUISpinner = ora("Configuring OAuth UI...").start();
484
- try {
485
- await this.updateOAuthUIConfig(options.oauthProviders, options.emailPasswordAuth);
486
- oauthUISpinner.succeed(
487
- options.oauthProviders.length > 0 ? "OAuth UI configuration updated" : "OAuth UI placeholders cleaned up"
488
- );
489
- } catch (error) {
490
- oauthUISpinner.fail("Failed to configure OAuth UI");
491
- throw error;
492
- }
493
- const envSpinner = ora("Updating .env.example...").start();
494
- try {
495
- await this.updateEnvExample(options.oauthProviders);
496
- envSpinner.succeed(
497
- options.oauthProviders.length > 0 ? ".env.example updated" : ".env.example placeholders cleaned up"
498
- );
499
- } catch (error) {
500
- envSpinner.fail("Failed to update .env.example");
501
- throw error;
502
- }
503
- const envTsSpinner = ora("Updating typed env configuration...").start();
504
- try {
505
- await this.updateEnvTs(options.oauthProviders);
506
- envTsSpinner.succeed(
507
- options.oauthProviders.length > 0 ? "Typed env configuration updated" : "Typed env placeholders cleaned up"
508
- );
509
- } catch (error) {
510
- envTsSpinner.fail("Failed to update typed env configuration");
511
- throw error;
512
- }
513
- const readmeSpinner = ora("Updating README...").start();
514
- try {
515
- await this.updateReadme(options.oauthProviders);
516
- readmeSpinner.succeed(
517
- options.oauthProviders.length > 0 ? "README updated" : "README placeholders cleaned up"
518
- );
519
- } catch (error) {
520
- readmeSpinner.fail("Failed to update README");
521
- throw error;
522
- }
523
- const orgsSpinner = ora("Configuring organizations...").start();
524
- try {
525
- await this.configureOrganizations(options.orgs);
526
- orgsSpinner.succeed(
527
- options.orgs ? "Organizations plugin enabled" : "Organizations placeholders cleaned up"
528
- );
529
- } catch (error) {
530
- orgsSpinner.fail("Failed to configure organizations");
531
- throw error;
532
- }
533
- const secretSpinner = ora("Generating auth secret...").start();
534
- try {
535
- await this.writeAuthSecret();
536
- secretSpinner.succeed("Auth secret generated");
537
- } catch (error) {
538
- secretSpinner.fail("Failed to generate auth secret");
539
- throw error;
540
- }
541
- if (options.monorepo) {
542
- const monorepoSpinner = ora("Rewriting dependencies for the host workspace...").start();
543
- try {
544
- await this.applyMonorepoRewrite(options);
545
- monorepoSpinner.succeed("Dependencies rewritten for the host workspace");
546
- } catch (error) {
547
- monorepoSpinner.fail("Failed to rewrite dependencies for the host workspace");
548
- throw error;
549
- }
550
- }
551
- if (options.initGit && !options.monorepo) {
552
- await this.initGitRepo();
553
- }
554
- if (options.installDependencies && !options.monorepo) {
555
- await this.installDependencies();
556
- await this.lintCode();
557
- await this.formatCode();
558
- }
559
- }
560
- };
561
-
562
- // src/installers/nextjs.ts
563
- import { join as join2 } from "path";
494
+ ],
495
+ docs: {
496
+ provider: "https://developer.twitter.com/en/portal/projects-and-apps",
497
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
498
+ },
499
+ requiresExtraConfig: false,
500
+ extraConfigNotes: "",
501
+ readme: {
502
+ title: "Twitter/X OAuth Setup",
503
+ content: `## Twitter/X OAuth Setup
564
504
 
565
- // src/installers/string-utils.ts
566
- import fs5 from "fs-extra";
505
+ 1. Create an app at https://developer.twitter.com/en/portal/projects-and-apps
506
+ 2. Enable OAuth 2.0 authentication
507
+ 3. Add callback URL: \`http://localhost:3000/api/auth/callback/twitter\` (update for production)
508
+ 4. Copy the Client ID and Client Secret to your \`.env\` file
567
509
 
568
- // src/installers/providers.ts
569
- var OAUTH_PROVIDERS = {
570
- // ========================================
571
- // POPULAR PROVIDERS (10)
572
- // ========================================
573
- google: {
574
- id: "google",
575
- name: "Google",
576
- envPrefix: "GOOGLE",
577
- clientIdVar: "GOOGLE_CLIENT_ID",
578
- clientSecretVar: "GOOGLE_CLIENT_SECRET",
510
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
511
+ }
512
+ },
513
+ linkedin: {
514
+ id: "linkedin",
515
+ name: "LinkedIn",
516
+ envPrefix: "LINKEDIN",
517
+ clientIdVar: "LINKEDIN_CLIENT_ID",
518
+ clientSecretVar: "LINKEDIN_CLIENT_SECRET",
579
519
  popular: true,
580
520
  betterAuthConfig: {
581
521
  import: "",
582
- clientSideProvider: '"google"',
583
- socialProvider: `google({
584
- clientId: process.env.GOOGLE_CLIENT_ID!,
585
- clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
522
+ clientSideProvider: '"linkedin"',
523
+ socialProvider: `linkedin({
524
+ clientId: process.env.LINKEDIN_CLIENT_ID!,
525
+ clientSecret: process.env.LINKEDIN_CLIENT_SECRET!,
586
526
  })`,
587
527
  scopes: []
588
528
  },
589
529
  env: [
590
530
  {
591
- name: "GOOGLE_CLIENT_ID",
531
+ name: "LINKEDIN_CLIENT_ID",
592
532
  type: "server",
593
- description: "Google OAuth Client ID"
533
+ description: "LinkedIn OAuth Client ID"
594
534
  },
595
535
  {
596
- name: "GOOGLE_CLIENT_SECRET",
536
+ name: "LINKEDIN_CLIENT_SECRET",
597
537
  type: "server",
598
- description: "Google OAuth Client Secret"
538
+ description: "LinkedIn OAuth Client Secret"
599
539
  }
600
540
  ],
601
541
  docs: {
602
- provider: "https://console.cloud.google.com/apis/credentials",
542
+ provider: "https://www.linkedin.com/developers/apps",
603
543
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
604
544
  },
605
545
  requiresExtraConfig: false,
606
546
  extraConfigNotes: "",
607
547
  readme: {
608
- title: "Google OAuth Setup",
609
- content: `## Google OAuth Setup
548
+ title: "LinkedIn OAuth Setup",
549
+ content: `## LinkedIn OAuth Setup
610
550
 
611
- 1. Create OAuth credentials at https://console.cloud.google.com/apis/credentials
612
- 2. Set the Authorized redirect URI to: \`http://localhost:3000/api/auth/callback/google\` (update for production)
613
- 3. Copy the Client ID and Client Secret to your \`.env\` file
551
+ 1. Create an app at https://www.linkedin.com/developers/apps
552
+ 2. Add redirect URL: \`http://localhost:3000/api/auth/callback/linkedin\` (update for production)
553
+ 3. Request access to Sign In with LinkedIn
554
+ 4. Copy the Client ID and Client Secret to your \`.env\` file
614
555
 
615
556
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
616
557
  }
617
558
  },
618
- github: {
619
- id: "github",
620
- name: "GitHub",
621
- envPrefix: "GITHUB",
622
- clientIdVar: "GITHUB_CLIENT_ID",
623
- clientSecretVar: "GITHUB_CLIENT_SECRET",
559
+ twitch: {
560
+ id: "twitch",
561
+ name: "Twitch",
562
+ envPrefix: "TWITCH",
563
+ clientIdVar: "TWITCH_CLIENT_ID",
564
+ clientSecretVar: "TWITCH_CLIENT_SECRET",
624
565
  popular: true,
625
566
  betterAuthConfig: {
626
567
  import: "",
627
- clientSideProvider: '"github"',
628
- socialProvider: `github({
629
- clientId: process.env.GITHUB_CLIENT_ID!,
630
- clientSecret: process.env.GITHUB_CLIENT_SECRET!,
568
+ clientSideProvider: '"twitch"',
569
+ socialProvider: `twitch({
570
+ clientId: process.env.TWITCH_CLIENT_ID!,
571
+ clientSecret: process.env.TWITCH_CLIENT_SECRET!,
631
572
  })`,
632
- scopes: ["user:email"]
573
+ scopes: []
633
574
  },
634
575
  env: [
635
576
  {
636
- name: "GITHUB_CLIENT_ID",
577
+ name: "TWITCH_CLIENT_ID",
637
578
  type: "server",
638
- description: "GitHub OAuth App Client ID"
579
+ description: "Twitch Application Client ID"
639
580
  },
640
581
  {
641
- name: "GITHUB_CLIENT_SECRET",
582
+ name: "TWITCH_CLIENT_SECRET",
642
583
  type: "server",
643
- description: "GitHub OAuth App Client Secret"
584
+ description: "Twitch Application Client Secret"
644
585
  }
645
586
  ],
646
587
  docs: {
647
- provider: "https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app",
588
+ provider: "https://dev.twitch.tv/console/apps",
648
589
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
649
590
  },
650
- requiresExtraConfig: true,
651
- extraConfigNotes: "You MUST include the user:email scope in your GitHub app. For GitHub Apps, enable Read-Only access to Email Addresses in Permissions.",
591
+ requiresExtraConfig: false,
592
+ extraConfigNotes: "",
652
593
  readme: {
653
- title: "GitHub OAuth Setup",
654
- content: `## GitHub OAuth Setup
594
+ title: "Twitch OAuth Setup",
595
+ content: `## Twitch OAuth Setup
655
596
 
656
- 1. Create a GitHub OAuth App at https://github.com/settings/developers
657
- 2. Set the Authorization callback URL to: \`http://localhost:3000/api/auth/callback/github\` (update for production)
658
- 3. Copy the Client ID and Client Secret to your \`.env\` file
659
- 4. **Important**: Include the \`user:email\` scope in your GitHub app permissions
597
+ 1. Register an application at https://dev.twitch.tv/console/apps
598
+ 2. Set OAuth Redirect URL to: \`http://localhost:3000/api/auth/callback/twitch\` (update for production)
599
+ 3. Copy the Client ID and generate a Client Secret
600
+ 4. Add credentials to your \`.env\` file
660
601
 
661
602
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
662
603
  }
663
604
  },
664
- discord: {
665
- id: "discord",
666
- name: "Discord",
667
- envPrefix: "DISCORD",
668
- clientIdVar: "DISCORD_CLIENT_ID",
669
- clientSecretVar: "DISCORD_CLIENT_SECRET",
605
+ spotify: {
606
+ id: "spotify",
607
+ name: "Spotify",
608
+ envPrefix: "SPOTIFY",
609
+ clientIdVar: "SPOTIFY_CLIENT_ID",
610
+ clientSecretVar: "SPOTIFY_CLIENT_SECRET",
670
611
  popular: true,
671
612
  betterAuthConfig: {
672
613
  import: "",
673
- clientSideProvider: '"discord"',
674
- socialProvider: `discord({
675
- clientId: process.env.DISCORD_CLIENT_ID!,
676
- clientSecret: process.env.DISCORD_CLIENT_SECRET!,
614
+ clientSideProvider: '"spotify"',
615
+ socialProvider: `spotify({
616
+ clientId: process.env.SPOTIFY_CLIENT_ID!,
617
+ clientSecret: process.env.SPOTIFY_CLIENT_SECRET!,
677
618
  })`,
678
619
  scopes: []
679
620
  },
680
621
  env: [
681
622
  {
682
- name: "DISCORD_CLIENT_ID",
623
+ name: "SPOTIFY_CLIENT_ID",
683
624
  type: "server",
684
- description: "Discord OAuth Application Client ID"
625
+ description: "Spotify App Client ID"
685
626
  },
686
627
  {
687
- name: "DISCORD_CLIENT_SECRET",
628
+ name: "SPOTIFY_CLIENT_SECRET",
688
629
  type: "server",
689
- description: "Discord OAuth Application Client Secret"
630
+ description: "Spotify App Client Secret"
690
631
  }
691
632
  ],
692
633
  docs: {
693
- provider: "https://discord.com/developers/applications",
634
+ provider: "https://developer.spotify.com/dashboard/applications",
694
635
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
695
636
  },
696
637
  requiresExtraConfig: false,
697
- extraConfigNotes: "You can optionally add a permissions field to request additional Discord permissions.",
638
+ extraConfigNotes: "",
698
639
  readme: {
699
- title: "Discord OAuth Setup",
700
- content: `## Discord OAuth Setup
640
+ title: "Spotify OAuth Setup",
641
+ content: `## Spotify OAuth Setup
701
642
 
702
- 1. Create an application at https://discord.com/developers/applications
703
- 2. Add a redirect URL: \`http://localhost:3000/api/auth/callback/discord\` (update for production)
643
+ 1. Create an app at https://developer.spotify.com/dashboard/applications
644
+ 2. Add redirect URI: \`http://localhost:3000/api/auth/callback/spotify\` (update for production)
704
645
  3. Copy the Client ID and Client Secret to your \`.env\` file
705
646
 
706
647
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
707
648
  }
708
649
  },
709
- apple: {
710
- id: "apple",
711
- name: "Apple",
712
- envPrefix: "APPLE",
713
- clientIdVar: "APPLE_CLIENT_ID",
714
- clientSecretVar: "APPLE_CLIENT_SECRET",
715
- popular: true,
650
+ // ========================================
651
+ // ADDITIONAL PROVIDERS (23)
652
+ // ========================================
653
+ atlassian: {
654
+ id: "atlassian",
655
+ name: "Atlassian",
656
+ envPrefix: "ATLASSIAN",
657
+ clientIdVar: "ATLASSIAN_CLIENT_ID",
658
+ clientSecretVar: "ATLASSIAN_CLIENT_SECRET",
659
+ popular: false,
716
660
  betterAuthConfig: {
717
661
  import: "",
718
- clientSideProvider: '"apple"',
719
- socialProvider: `apple({
720
- clientId: process.env.APPLE_CLIENT_ID!,
721
- clientSecret: process.env.APPLE_CLIENT_SECRET!,
662
+ clientSideProvider: '"atlassian"',
663
+ socialProvider: `atlassian({
664
+ clientId: process.env.ATLASSIAN_CLIENT_ID!,
665
+ clientSecret: process.env.ATLASSIAN_CLIENT_SECRET!,
722
666
  })`,
723
667
  scopes: []
724
668
  },
725
669
  env: [
726
670
  {
727
- name: "APPLE_CLIENT_ID",
671
+ name: "ATLASSIAN_CLIENT_ID",
728
672
  type: "server",
729
- description: "Apple Sign In Service ID"
673
+ description: "Atlassian OAuth 2.0 Client ID"
730
674
  },
731
675
  {
732
- name: "APPLE_CLIENT_SECRET",
676
+ name: "ATLASSIAN_CLIENT_SECRET",
733
677
  type: "server",
734
- description: "Apple Sign In Client Secret (JWT token)"
678
+ description: "Atlassian OAuth 2.0 Client Secret"
735
679
  }
736
680
  ],
737
681
  docs: {
738
- provider: "https://developer.apple.com/sign-in-with-apple/get-started/",
682
+ provider: "https://developer.atlassian.com/console/myapps/",
739
683
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
740
684
  },
741
- requiresExtraConfig: true,
742
- extraConfigNotes: "Apple requires additional configuration: clientId is your Service ID, clientSecret must be a JWT token generated from your Team ID, Key ID, and Private Key. You may also need to provide appBundleIdentifier for iOS apps.",
685
+ requiresExtraConfig: false,
686
+ extraConfigNotes: "Default scopes include read:jira-user, read:jira-work, and offline_access.",
743
687
  readme: {
744
- title: "Apple Sign In Setup",
745
- content: `## Apple Sign In Setup
746
-
747
- 1. Create a Sign In with Apple service at https://developer.apple.com
748
- 2. Configure your Service ID and return URLs
749
- 3. Generate a client secret JWT using your private key, Team ID, and Key ID
750
- 4. Add credentials to your \`.env\` file
688
+ title: "Atlassian OAuth Setup",
689
+ content: `## Atlassian OAuth Setup
751
690
 
752
- **Note**: The clientSecret must be a JWT token generated from your Apple credentials, not a standard client secret.
691
+ 1. Create an app at https://developer.atlassian.com/console/myapps/
692
+ 2. Configure OAuth 2.0 integration
693
+ 3. Add callback URL: \`http://localhost:3000/api/auth/callback/atlassian\` (update for production)
694
+ 4. Copy the Client ID and Client Secret to your \`.env\` file
753
695
 
754
696
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
755
697
  }
756
698
  },
757
- microsoft: {
758
- id: "microsoft",
759
- name: "Microsoft",
760
- envPrefix: "MICROSOFT",
761
- clientIdVar: "MICROSOFT_CLIENT_ID",
762
- clientSecretVar: "MICROSOFT_CLIENT_SECRET",
763
- popular: true,
699
+ cognito: {
700
+ id: "cognito",
701
+ name: "AWS Cognito",
702
+ envPrefix: "COGNITO",
703
+ clientIdVar: "COGNITO_CLIENT_ID",
704
+ clientSecretVar: "COGNITO_CLIENT_SECRET",
705
+ popular: false,
764
706
  betterAuthConfig: {
765
707
  import: "",
766
- clientSideProvider: '"microsoft"',
767
- socialProvider: `microsoft({
768
- clientId: process.env.MICROSOFT_CLIENT_ID!,
769
- clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
770
- tenantId: process.env.MICROSOFT_TENANT_ID,
708
+ clientSideProvider: '"cognito"',
709
+ socialProvider: `cognito({
710
+ clientId: process.env.COGNITO_CLIENT_ID!,
711
+ clientSecret: process.env.COGNITO_CLIENT_SECRET!,
712
+ domain: process.env.COGNITO_DOMAIN!,
713
+ region: process.env.COGNITO_REGION!,
714
+ userPoolId: process.env.COGNITO_USER_POOL_ID!,
771
715
  })`,
772
716
  scopes: []
773
717
  },
774
718
  env: [
775
719
  {
776
- name: "MICROSOFT_CLIENT_ID",
720
+ name: "COGNITO_CLIENT_ID",
777
721
  type: "server",
778
- description: "Microsoft Entra ID Application (client) ID"
722
+ description: "AWS Cognito App Client ID"
779
723
  },
780
724
  {
781
- name: "MICROSOFT_CLIENT_SECRET",
725
+ name: "COGNITO_CLIENT_SECRET",
782
726
  type: "server",
783
- description: "Microsoft Entra ID Client Secret"
727
+ description: "AWS Cognito App Client Secret"
784
728
  },
785
729
  {
786
- name: "MICROSOFT_TENANT_ID",
730
+ name: "COGNITO_DOMAIN",
787
731
  type: "server",
788
- description: 'Microsoft Entra ID Tenant ID (optional, defaults to "common")'
732
+ description: "AWS Cognito domain (e.g., your-domain.auth.us-east-1.amazoncognito.com)"
733
+ },
734
+ {
735
+ name: "COGNITO_REGION",
736
+ type: "server",
737
+ description: "AWS region (e.g., us-east-1)"
738
+ },
739
+ {
740
+ name: "COGNITO_USER_POOL_ID",
741
+ type: "server",
742
+ description: "AWS Cognito User Pool ID"
789
743
  }
790
744
  ],
791
745
  docs: {
792
- provider: "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps",
746
+ provider: "https://console.aws.amazon.com/cognito/",
793
747
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
794
748
  },
795
749
  requiresExtraConfig: true,
796
- extraConfigNotes: 'Optional fields: tenantId (defaults to "common"), authority (custom authority URL), prompt (consent behavior).',
750
+ extraConfigNotes: "AWS Cognito requires domain, region, and userPoolId in addition to clientId and clientSecret.",
797
751
  readme: {
798
- title: "Microsoft Entra ID OAuth Setup",
799
- content: `## Microsoft Entra ID OAuth Setup
752
+ title: "AWS Cognito OAuth Setup",
753
+ content: `## AWS Cognito OAuth Setup
800
754
 
801
- 1. Register an application at https://portal.azure.com
802
- 2. Add a redirect URI: \`http://localhost:3000/api/auth/callback/microsoft\` (update for production)
803
- 3. Create a client secret in "Certificates & secrets"
804
- 4. Copy the Application (client) ID and Client Secret to your \`.env\` file
805
- 5. (Optional) Copy the Directory (tenant) ID if you want to restrict to a specific tenant
755
+ 1. Create a User Pool at https://console.aws.amazon.com/cognito/
756
+ 2. Configure an App Client with OAuth 2.0 flows
757
+ 3. Set up a Cognito domain for your user pool
758
+ 4. Add callback URL: \`http://localhost:3000/api/auth/callback/cognito\` (update for production)
759
+ 5. Copy the following to your \`.env\` file:
760
+ - App Client ID
761
+ - App Client Secret
762
+ - Cognito Domain
763
+ - AWS Region
764
+ - User Pool ID
806
765
 
807
766
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
808
767
  }
809
768
  },
810
- facebook: {
811
- id: "facebook",
812
- name: "Facebook",
813
- envPrefix: "FACEBOOK",
814
- clientIdVar: "FACEBOOK_CLIENT_ID",
815
- clientSecretVar: "FACEBOOK_CLIENT_SECRET",
816
- popular: true,
769
+ dropbox: {
770
+ id: "dropbox",
771
+ name: "Dropbox",
772
+ envPrefix: "DROPBOX",
773
+ clientIdVar: "DROPBOX_CLIENT_ID",
774
+ clientSecretVar: "DROPBOX_CLIENT_SECRET",
775
+ popular: false,
817
776
  betterAuthConfig: {
818
777
  import: "",
819
- clientSideProvider: '"facebook"',
820
- socialProvider: `facebook({
821
- clientId: process.env.FACEBOOK_CLIENT_ID!,
822
- clientSecret: process.env.FACEBOOK_CLIENT_SECRET!,
778
+ clientSideProvider: '"dropbox"',
779
+ socialProvider: `dropbox({
780
+ clientId: process.env.DROPBOX_CLIENT_ID!,
781
+ clientSecret: process.env.DROPBOX_CLIENT_SECRET!,
823
782
  })`,
824
783
  scopes: []
825
784
  },
826
785
  env: [
827
786
  {
828
- name: "FACEBOOK_CLIENT_ID",
787
+ name: "DROPBOX_CLIENT_ID",
829
788
  type: "server",
830
- description: "Facebook App ID"
789
+ description: "Dropbox App Key"
831
790
  },
832
791
  {
833
- name: "FACEBOOK_CLIENT_SECRET",
792
+ name: "DROPBOX_CLIENT_SECRET",
834
793
  type: "server",
835
- description: "Facebook App Secret"
794
+ description: "Dropbox App Secret"
836
795
  }
837
796
  ],
838
797
  docs: {
839
- provider: "https://developers.facebook.com/apps/",
798
+ provider: "https://www.dropbox.com/developers/apps",
840
799
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
841
800
  },
842
801
  requiresExtraConfig: false,
843
- extraConfigNotes: "Facebook supports custom scopes and fields arrays to request additional user data.",
802
+ extraConfigNotes: "",
844
803
  readme: {
845
- title: "Facebook OAuth Setup",
846
- content: `## Facebook OAuth Setup
804
+ title: "Dropbox OAuth Setup",
805
+ content: `## Dropbox OAuth Setup
847
806
 
848
- 1. Create an app at https://developers.facebook.com/apps/
849
- 2. Add Facebook Login product to your app
850
- 3. Add OAuth redirect URI: \`http://localhost:3000/api/auth/callback/facebook\` (update for production)
851
- 4. Copy the App ID and App Secret to your \`.env\` file
807
+ 1. Create an app at https://www.dropbox.com/developers/apps
808
+ 2. Choose OAuth 2 settings
809
+ 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/dropbox\` (update for production)
810
+ 4. Copy the App Key and App Secret to your \`.env\` file
852
811
 
853
812
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
854
813
  }
855
814
  },
856
- twitter: {
857
- id: "twitter",
858
- name: "Twitter/X",
859
- envPrefix: "TWITTER",
860
- clientIdVar: "TWITTER_CLIENT_ID",
861
- clientSecretVar: "TWITTER_CLIENT_SECRET",
862
- popular: true,
815
+ figma: {
816
+ id: "figma",
817
+ name: "Figma",
818
+ envPrefix: "FIGMA",
819
+ clientIdVar: "FIGMA_CLIENT_ID",
820
+ clientSecretVar: "FIGMA_CLIENT_SECRET",
821
+ popular: false,
863
822
  betterAuthConfig: {
864
823
  import: "",
865
- clientSideProvider: '"twitter"',
866
- socialProvider: `twitter({
867
- clientId: process.env.TWITTER_CLIENT_ID!,
868
- clientSecret: process.env.TWITTER_CLIENT_SECRET!,
824
+ clientSideProvider: '"figma"',
825
+ socialProvider: `figma({
826
+ clientId: process.env.FIGMA_CLIENT_ID!,
827
+ clientSecret: process.env.FIGMA_CLIENT_SECRET!,
828
+ clientKey: process.env.FIGMA_CLIENT_KEY!,
869
829
  })`,
870
830
  scopes: []
871
831
  },
872
832
  env: [
873
833
  {
874
- name: "TWITTER_CLIENT_ID",
834
+ name: "FIGMA_CLIENT_ID",
875
835
  type: "server",
876
- description: "Twitter/X OAuth 2.0 Client ID"
836
+ description: "Figma OAuth Client ID"
877
837
  },
878
838
  {
879
- name: "TWITTER_CLIENT_SECRET",
839
+ name: "FIGMA_CLIENT_SECRET",
880
840
  type: "server",
881
- description: "Twitter/X OAuth 2.0 Client Secret"
841
+ description: "Figma OAuth Client Secret"
842
+ },
843
+ {
844
+ name: "FIGMA_CLIENT_KEY",
845
+ type: "server",
846
+ description: "Figma OAuth Client Key"
882
847
  }
883
848
  ],
884
849
  docs: {
885
- provider: "https://developer.twitter.com/en/portal/projects-and-apps",
850
+ provider: "https://www.figma.com/developers/apps",
886
851
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
887
852
  },
888
- requiresExtraConfig: false,
889
- extraConfigNotes: "",
853
+ requiresExtraConfig: true,
854
+ extraConfigNotes: "Figma requires a clientKey in addition to clientId and clientSecret.",
890
855
  readme: {
891
- title: "Twitter/X OAuth Setup",
892
- content: `## Twitter/X OAuth Setup
856
+ title: "Figma OAuth Setup",
857
+ content: `## Figma OAuth Setup
893
858
 
894
- 1. Create an app at https://developer.twitter.com/en/portal/projects-and-apps
895
- 2. Enable OAuth 2.0 authentication
896
- 3. Add callback URL: \`http://localhost:3000/api/auth/callback/twitter\` (update for production)
897
- 4. Copy the Client ID and Client Secret to your \`.env\` file
859
+ 1. Create an app at https://www.figma.com/developers/apps
860
+ 2. Configure OAuth settings
861
+ 3. Add callback URL: \`http://localhost:3000/api/auth/callback/figma\` (update for production)
862
+ 4. Copy the Client ID, Client Secret, and Client Key to your \`.env\` file
898
863
 
899
864
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
900
865
  }
901
866
  },
902
- linkedin: {
903
- id: "linkedin",
904
- name: "LinkedIn",
905
- envPrefix: "LINKEDIN",
906
- clientIdVar: "LINKEDIN_CLIENT_ID",
907
- clientSecretVar: "LINKEDIN_CLIENT_SECRET",
908
- popular: true,
867
+ gitlab: {
868
+ id: "gitlab",
869
+ name: "GitLab",
870
+ envPrefix: "GITLAB",
871
+ clientIdVar: "GITLAB_CLIENT_ID",
872
+ clientSecretVar: "GITLAB_CLIENT_SECRET",
873
+ popular: false,
909
874
  betterAuthConfig: {
910
875
  import: "",
911
- clientSideProvider: '"linkedin"',
912
- socialProvider: `linkedin({
913
- clientId: process.env.LINKEDIN_CLIENT_ID!,
914
- clientSecret: process.env.LINKEDIN_CLIENT_SECRET!,
876
+ clientSideProvider: '"gitlab"',
877
+ socialProvider: `gitlab({
878
+ clientId: process.env.GITLAB_CLIENT_ID!,
879
+ clientSecret: process.env.GITLAB_CLIENT_SECRET!,
915
880
  })`,
916
881
  scopes: []
917
882
  },
918
883
  env: [
919
884
  {
920
- name: "LINKEDIN_CLIENT_ID",
885
+ name: "GITLAB_CLIENT_ID",
921
886
  type: "server",
922
- description: "LinkedIn OAuth Client ID"
887
+ description: "GitLab Application ID"
923
888
  },
924
889
  {
925
- name: "LINKEDIN_CLIENT_SECRET",
890
+ name: "GITLAB_CLIENT_SECRET",
926
891
  type: "server",
927
- description: "LinkedIn OAuth Client Secret"
892
+ description: "GitLab Application Secret"
928
893
  }
929
894
  ],
930
895
  docs: {
931
- provider: "https://www.linkedin.com/developers/apps",
896
+ provider: "https://gitlab.com/-/profile/applications",
932
897
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
933
898
  },
934
899
  requiresExtraConfig: false,
935
- extraConfigNotes: "",
900
+ extraConfigNotes: "Optionally supports an issuer field for self-hosted GitLab instances.",
936
901
  readme: {
937
- title: "LinkedIn OAuth Setup",
938
- content: `## LinkedIn OAuth Setup
902
+ title: "GitLab OAuth Setup",
903
+ content: `## GitLab OAuth Setup
939
904
 
940
- 1. Create an app at https://www.linkedin.com/developers/apps
941
- 2. Add redirect URL: \`http://localhost:3000/api/auth/callback/linkedin\` (update for production)
942
- 3. Request access to Sign In with LinkedIn
943
- 4. Copy the Client ID and Client Secret to your \`.env\` file
905
+ 1. Create an application at https://gitlab.com/-/profile/applications
906
+ 2. Add redirect URI: \`http://localhost:3000/api/auth/callback/gitlab\` (update for production)
907
+ 3. Select the required scopes (read_user is recommended)
908
+ 4. Copy the Application ID and Secret to your \`.env\` file
944
909
 
945
910
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
946
911
  }
947
912
  },
948
- twitch: {
949
- id: "twitch",
950
- name: "Twitch",
951
- envPrefix: "TWITCH",
952
- clientIdVar: "TWITCH_CLIENT_ID",
953
- clientSecretVar: "TWITCH_CLIENT_SECRET",
954
- popular: true,
913
+ huggingface: {
914
+ id: "huggingface",
915
+ name: "Hugging Face",
916
+ envPrefix: "HUGGINGFACE",
917
+ clientIdVar: "HUGGINGFACE_CLIENT_ID",
918
+ clientSecretVar: "HUGGINGFACE_CLIENT_SECRET",
919
+ popular: false,
955
920
  betterAuthConfig: {
956
921
  import: "",
957
- clientSideProvider: '"twitch"',
958
- socialProvider: `twitch({
959
- clientId: process.env.TWITCH_CLIENT_ID!,
960
- clientSecret: process.env.TWITCH_CLIENT_SECRET!,
922
+ clientSideProvider: '"huggingface"',
923
+ socialProvider: `huggingface({
924
+ clientId: process.env.HUGGINGFACE_CLIENT_ID!,
925
+ clientSecret: process.env.HUGGINGFACE_CLIENT_SECRET!,
961
926
  })`,
962
- scopes: []
927
+ scopes: ["email"]
963
928
  },
964
929
  env: [
965
930
  {
966
- name: "TWITCH_CLIENT_ID",
931
+ name: "HUGGINGFACE_CLIENT_ID",
967
932
  type: "server",
968
- description: "Twitch Application Client ID"
933
+ description: "Hugging Face OAuth Client ID"
969
934
  },
970
935
  {
971
- name: "TWITCH_CLIENT_SECRET",
936
+ name: "HUGGINGFACE_CLIENT_SECRET",
972
937
  type: "server",
973
- description: "Twitch Application Client Secret"
938
+ description: "Hugging Face OAuth Client Secret"
974
939
  }
975
940
  ],
976
941
  docs: {
977
- provider: "https://dev.twitch.tv/console/apps",
942
+ provider: "https://huggingface.co/settings/connected-applications",
978
943
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
979
944
  },
980
- requiresExtraConfig: false,
981
- extraConfigNotes: "",
945
+ requiresExtraConfig: true,
946
+ extraConfigNotes: "You MUST include the email scope for Hugging Face.",
982
947
  readme: {
983
- title: "Twitch OAuth Setup",
984
- content: `## Twitch OAuth Setup
948
+ title: "Hugging Face OAuth Setup",
949
+ content: `## Hugging Face OAuth Setup
985
950
 
986
- 1. Register an application at https://dev.twitch.tv/console/apps
987
- 2. Set OAuth Redirect URL to: \`http://localhost:3000/api/auth/callback/twitch\` (update for production)
988
- 3. Copy the Client ID and generate a Client Secret
989
- 4. Add credentials to your \`.env\` file
951
+ 1. Create an OAuth app at https://huggingface.co/settings/connected-applications
952
+ 2. Set redirect URI to: \`http://localhost:3000/api/auth/callback/huggingface\` (update for production)
953
+ 3. Copy the Client ID and Client Secret to your \`.env\` file
954
+ 4. **Important**: Make sure to include the \`email\` scope in your configuration
990
955
 
991
956
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
992
957
  }
993
958
  },
994
- spotify: {
995
- id: "spotify",
996
- name: "Spotify",
997
- envPrefix: "SPOTIFY",
998
- clientIdVar: "SPOTIFY_CLIENT_ID",
999
- clientSecretVar: "SPOTIFY_CLIENT_SECRET",
1000
- popular: true,
959
+ kakao: {
960
+ id: "kakao",
961
+ name: "Kakao",
962
+ envPrefix: "KAKAO",
963
+ clientIdVar: "KAKAO_CLIENT_ID",
964
+ clientSecretVar: "KAKAO_CLIENT_SECRET",
965
+ popular: false,
1001
966
  betterAuthConfig: {
1002
967
  import: "",
1003
- clientSideProvider: '"spotify"',
1004
- socialProvider: `spotify({
1005
- clientId: process.env.SPOTIFY_CLIENT_ID!,
1006
- clientSecret: process.env.SPOTIFY_CLIENT_SECRET!,
968
+ clientSideProvider: '"kakao"',
969
+ socialProvider: `kakao({
970
+ clientId: process.env.KAKAO_CLIENT_ID!,
971
+ clientSecret: process.env.KAKAO_CLIENT_SECRET!,
1007
972
  })`,
1008
973
  scopes: []
1009
974
  },
1010
975
  env: [
1011
976
  {
1012
- name: "SPOTIFY_CLIENT_ID",
977
+ name: "KAKAO_CLIENT_ID",
1013
978
  type: "server",
1014
- description: "Spotify App Client ID"
979
+ description: "Kakao REST API Key"
1015
980
  },
1016
981
  {
1017
- name: "SPOTIFY_CLIENT_SECRET",
982
+ name: "KAKAO_CLIENT_SECRET",
1018
983
  type: "server",
1019
- description: "Spotify App Client Secret"
984
+ description: "Kakao Client Secret"
1020
985
  }
1021
986
  ],
1022
987
  docs: {
1023
- provider: "https://developer.spotify.com/dashboard/applications",
988
+ provider: "https://developers.kakao.com/console/app",
1024
989
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1025
990
  },
1026
991
  requiresExtraConfig: false,
1027
992
  extraConfigNotes: "",
1028
993
  readme: {
1029
- title: "Spotify OAuth Setup",
1030
- content: `## Spotify OAuth Setup
994
+ title: "Kakao OAuth Setup",
995
+ content: `## Kakao OAuth Setup
1031
996
 
1032
- 1. Create an app at https://developer.spotify.com/dashboard/applications
1033
- 2. Add redirect URI: \`http://localhost:3000/api/auth/callback/spotify\` (update for production)
1034
- 3. Copy the Client ID and Client Secret to your \`.env\` file
997
+ 1. Create an application at https://developers.kakao.com/console/app
998
+ 2. Enable Kakao Login in the app settings
999
+ 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/kakao\` (update for production)
1000
+ 4. Copy the REST API Key and Client Secret to your \`.env\` file
1035
1001
 
1036
1002
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1037
1003
  }
1038
1004
  },
1039
- // ========================================
1040
- // ADDITIONAL PROVIDERS (23)
1041
- // ========================================
1042
- atlassian: {
1043
- id: "atlassian",
1044
- name: "Atlassian",
1045
- envPrefix: "ATLASSIAN",
1046
- clientIdVar: "ATLASSIAN_CLIENT_ID",
1047
- clientSecretVar: "ATLASSIAN_CLIENT_SECRET",
1005
+ kick: {
1006
+ id: "kick",
1007
+ name: "Kick",
1008
+ envPrefix: "KICK",
1009
+ clientIdVar: "KICK_CLIENT_ID",
1010
+ clientSecretVar: "KICK_CLIENT_SECRET",
1048
1011
  popular: false,
1049
1012
  betterAuthConfig: {
1050
1013
  import: "",
1051
- clientSideProvider: '"atlassian"',
1052
- socialProvider: `atlassian({
1053
- clientId: process.env.ATLASSIAN_CLIENT_ID!,
1054
- clientSecret: process.env.ATLASSIAN_CLIENT_SECRET!,
1014
+ clientSideProvider: '"kick"',
1015
+ socialProvider: `kick({
1016
+ clientId: process.env.KICK_CLIENT_ID!,
1017
+ clientSecret: process.env.KICK_CLIENT_SECRET!,
1055
1018
  })`,
1056
1019
  scopes: []
1057
1020
  },
1058
1021
  env: [
1059
1022
  {
1060
- name: "ATLASSIAN_CLIENT_ID",
1023
+ name: "KICK_CLIENT_ID",
1061
1024
  type: "server",
1062
- description: "Atlassian OAuth 2.0 Client ID"
1025
+ description: "Kick OAuth Client ID"
1063
1026
  },
1064
1027
  {
1065
- name: "ATLASSIAN_CLIENT_SECRET",
1028
+ name: "KICK_CLIENT_SECRET",
1066
1029
  type: "server",
1067
- description: "Atlassian OAuth 2.0 Client Secret"
1030
+ description: "Kick OAuth Client Secret"
1068
1031
  }
1069
1032
  ],
1070
1033
  docs: {
1071
- provider: "https://developer.atlassian.com/console/myapps/",
1034
+ provider: "https://dev.kick.com/",
1072
1035
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1073
1036
  },
1074
1037
  requiresExtraConfig: false,
1075
- extraConfigNotes: "Default scopes include read:jira-user, read:jira-work, and offline_access.",
1038
+ extraConfigNotes: "",
1076
1039
  readme: {
1077
- title: "Atlassian OAuth Setup",
1078
- content: `## Atlassian OAuth Setup
1040
+ title: "Kick OAuth Setup",
1041
+ content: `## Kick OAuth Setup
1079
1042
 
1080
- 1. Create an app at https://developer.atlassian.com/console/myapps/
1081
- 2. Configure OAuth 2.0 integration
1082
- 3. Add callback URL: \`http://localhost:3000/api/auth/callback/atlassian\` (update for production)
1043
+ 1. Create an application at https://dev.kick.com/
1044
+ 2. Configure OAuth settings
1045
+ 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/kick\` (update for production)
1083
1046
  4. Copy the Client ID and Client Secret to your \`.env\` file
1084
1047
 
1085
1048
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1086
1049
  }
1087
1050
  },
1088
- cognito: {
1089
- id: "cognito",
1090
- name: "AWS Cognito",
1091
- envPrefix: "COGNITO",
1092
- clientIdVar: "COGNITO_CLIENT_ID",
1093
- clientSecretVar: "COGNITO_CLIENT_SECRET",
1051
+ line: {
1052
+ id: "line",
1053
+ name: "LINE",
1054
+ envPrefix: "LINE",
1055
+ clientIdVar: "LINE_CLIENT_ID",
1056
+ clientSecretVar: "LINE_CLIENT_SECRET",
1094
1057
  popular: false,
1095
1058
  betterAuthConfig: {
1096
1059
  import: "",
1097
- clientSideProvider: '"cognito"',
1098
- socialProvider: `cognito({
1099
- clientId: process.env.COGNITO_CLIENT_ID!,
1100
- clientSecret: process.env.COGNITO_CLIENT_SECRET!,
1101
- domain: process.env.COGNITO_DOMAIN!,
1102
- region: process.env.COGNITO_REGION!,
1103
- userPoolId: process.env.COGNITO_USER_POOL_ID!,
1060
+ clientSideProvider: '"line"',
1061
+ socialProvider: `line({
1062
+ clientId: process.env.LINE_CLIENT_ID!,
1063
+ clientSecret: process.env.LINE_CLIENT_SECRET!,
1104
1064
  })`,
1105
1065
  scopes: []
1106
1066
  },
1107
1067
  env: [
1108
1068
  {
1109
- name: "COGNITO_CLIENT_ID",
1110
- type: "server",
1111
- description: "AWS Cognito App Client ID"
1112
- },
1113
- {
1114
- name: "COGNITO_CLIENT_SECRET",
1115
- type: "server",
1116
- description: "AWS Cognito App Client Secret"
1117
- },
1118
- {
1119
- name: "COGNITO_DOMAIN",
1120
- type: "server",
1121
- description: "AWS Cognito domain (e.g., your-domain.auth.us-east-1.amazoncognito.com)"
1122
- },
1123
- {
1124
- name: "COGNITO_REGION",
1069
+ name: "LINE_CLIENT_ID",
1125
1070
  type: "server",
1126
- description: "AWS region (e.g., us-east-1)"
1071
+ description: "LINE Channel ID"
1127
1072
  },
1128
1073
  {
1129
- name: "COGNITO_USER_POOL_ID",
1074
+ name: "LINE_CLIENT_SECRET",
1130
1075
  type: "server",
1131
- description: "AWS Cognito User Pool ID"
1076
+ description: "LINE Channel Secret"
1132
1077
  }
1133
1078
  ],
1134
1079
  docs: {
1135
- provider: "https://console.aws.amazon.com/cognito/",
1080
+ provider: "https://developers.line.biz/console/",
1136
1081
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1137
1082
  },
1138
- requiresExtraConfig: true,
1139
- extraConfigNotes: "AWS Cognito requires domain, region, and userPoolId in addition to clientId and clientSecret.",
1083
+ requiresExtraConfig: false,
1084
+ extraConfigNotes: "Supports multi-channel configuration.",
1140
1085
  readme: {
1141
- title: "AWS Cognito OAuth Setup",
1142
- content: `## AWS Cognito OAuth Setup
1086
+ title: "LINE OAuth Setup",
1087
+ content: `## LINE OAuth Setup
1143
1088
 
1144
- 1. Create a User Pool at https://console.aws.amazon.com/cognito/
1145
- 2. Configure an App Client with OAuth 2.0 flows
1146
- 3. Set up a Cognito domain for your user pool
1147
- 4. Add callback URL: \`http://localhost:3000/api/auth/callback/cognito\` (update for production)
1148
- 5. Copy the following to your \`.env\` file:
1149
- - App Client ID
1150
- - App Client Secret
1151
- - Cognito Domain
1152
- - AWS Region
1153
- - User Pool ID
1089
+ 1. Create a channel at https://developers.line.biz/console/
1090
+ 2. Enable LINE Login
1091
+ 3. Add callback URL: \`http://localhost:3000/api/auth/callback/line\` (update for production)
1092
+ 4. Copy the Channel ID and Channel Secret to your \`.env\` file
1154
1093
 
1155
1094
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1156
1095
  }
1157
1096
  },
1158
- dropbox: {
1159
- id: "dropbox",
1160
- name: "Dropbox",
1161
- envPrefix: "DROPBOX",
1162
- clientIdVar: "DROPBOX_CLIENT_ID",
1163
- clientSecretVar: "DROPBOX_CLIENT_SECRET",
1097
+ linear: {
1098
+ id: "linear",
1099
+ name: "Linear",
1100
+ envPrefix: "LINEAR",
1101
+ clientIdVar: "LINEAR_CLIENT_ID",
1102
+ clientSecretVar: "LINEAR_CLIENT_SECRET",
1164
1103
  popular: false,
1165
1104
  betterAuthConfig: {
1166
1105
  import: "",
1167
- clientSideProvider: '"dropbox"',
1168
- socialProvider: `dropbox({
1169
- clientId: process.env.DROPBOX_CLIENT_ID!,
1170
- clientSecret: process.env.DROPBOX_CLIENT_SECRET!,
1106
+ clientSideProvider: '"linear"',
1107
+ socialProvider: `linear({
1108
+ clientId: process.env.LINEAR_CLIENT_ID!,
1109
+ clientSecret: process.env.LINEAR_CLIENT_SECRET!,
1171
1110
  })`,
1172
1111
  scopes: []
1173
1112
  },
1174
1113
  env: [
1175
1114
  {
1176
- name: "DROPBOX_CLIENT_ID",
1115
+ name: "LINEAR_CLIENT_ID",
1177
1116
  type: "server",
1178
- description: "Dropbox App Key"
1117
+ description: "Linear OAuth Client ID"
1179
1118
  },
1180
1119
  {
1181
- name: "DROPBOX_CLIENT_SECRET",
1120
+ name: "LINEAR_CLIENT_SECRET",
1182
1121
  type: "server",
1183
- description: "Dropbox App Secret"
1122
+ description: "Linear OAuth Client Secret"
1184
1123
  }
1185
1124
  ],
1186
1125
  docs: {
1187
- provider: "https://www.dropbox.com/developers/apps",
1126
+ provider: "https://linear.app/settings/api",
1188
1127
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1189
1128
  },
1190
1129
  requiresExtraConfig: false,
1191
- extraConfigNotes: "",
1130
+ extraConfigNotes: "Supports custom scope options.",
1192
1131
  readme: {
1193
- title: "Dropbox OAuth Setup",
1194
- content: `## Dropbox OAuth Setup
1132
+ title: "Linear OAuth Setup",
1133
+ content: `## Linear OAuth Setup
1195
1134
 
1196
- 1. Create an app at https://www.dropbox.com/developers/apps
1197
- 2. Choose OAuth 2 settings
1198
- 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/dropbox\` (update for production)
1199
- 4. Copy the App Key and App Secret to your \`.env\` file
1135
+ 1. Create an OAuth application at https://linear.app/settings/api
1136
+ 2. Add redirect URL: \`http://localhost:3000/api/auth/callback/linear\` (update for production)
1137
+ 3. Copy the Client ID and Client Secret to your \`.env\` file
1200
1138
 
1201
1139
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1202
1140
  }
1203
1141
  },
1204
- figma: {
1205
- id: "figma",
1206
- name: "Figma",
1207
- envPrefix: "FIGMA",
1208
- clientIdVar: "FIGMA_CLIENT_ID",
1209
- clientSecretVar: "FIGMA_CLIENT_SECRET",
1142
+ naver: {
1143
+ id: "naver",
1144
+ name: "Naver",
1145
+ envPrefix: "NAVER",
1146
+ clientIdVar: "NAVER_CLIENT_ID",
1147
+ clientSecretVar: "NAVER_CLIENT_SECRET",
1210
1148
  popular: false,
1211
1149
  betterAuthConfig: {
1212
1150
  import: "",
1213
- clientSideProvider: '"figma"',
1214
- socialProvider: `figma({
1215
- clientId: process.env.FIGMA_CLIENT_ID!,
1216
- clientSecret: process.env.FIGMA_CLIENT_SECRET!,
1217
- clientKey: process.env.FIGMA_CLIENT_KEY!,
1151
+ clientSideProvider: '"naver"',
1152
+ socialProvider: `naver({
1153
+ clientId: process.env.NAVER_CLIENT_ID!,
1154
+ clientSecret: process.env.NAVER_CLIENT_SECRET!,
1218
1155
  })`,
1219
1156
  scopes: []
1220
1157
  },
1221
1158
  env: [
1222
1159
  {
1223
- name: "FIGMA_CLIENT_ID",
1224
- type: "server",
1225
- description: "Figma OAuth Client ID"
1226
- },
1227
- {
1228
- name: "FIGMA_CLIENT_SECRET",
1160
+ name: "NAVER_CLIENT_ID",
1229
1161
  type: "server",
1230
- description: "Figma OAuth Client Secret"
1162
+ description: "Naver Client ID"
1231
1163
  },
1232
1164
  {
1233
- name: "FIGMA_CLIENT_KEY",
1165
+ name: "NAVER_CLIENT_SECRET",
1234
1166
  type: "server",
1235
- description: "Figma OAuth Client Key"
1167
+ description: "Naver Client Secret"
1236
1168
  }
1237
1169
  ],
1238
1170
  docs: {
1239
- provider: "https://www.figma.com/developers/apps",
1171
+ provider: "https://developers.naver.com/apps/",
1240
1172
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1241
1173
  },
1242
- requiresExtraConfig: true,
1243
- extraConfigNotes: "Figma requires a clientKey in addition to clientId and clientSecret.",
1174
+ requiresExtraConfig: false,
1175
+ extraConfigNotes: "",
1244
1176
  readme: {
1245
- title: "Figma OAuth Setup",
1246
- content: `## Figma OAuth Setup
1177
+ title: "Naver OAuth Setup",
1178
+ content: `## Naver OAuth Setup
1247
1179
 
1248
- 1. Create an app at https://www.figma.com/developers/apps
1249
- 2. Configure OAuth settings
1250
- 3. Add callback URL: \`http://localhost:3000/api/auth/callback/figma\` (update for production)
1251
- 4. Copy the Client ID, Client Secret, and Client Key to your \`.env\` file
1180
+ 1. Register an application at https://developers.naver.com/apps/
1181
+ 2. Configure Login API settings
1182
+ 3. Add callback URL: \`http://localhost:3000/api/auth/callback/naver\` (update for production)
1183
+ 4. Copy the Client ID and Client Secret to your \`.env\` file
1252
1184
 
1253
1185
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1254
1186
  }
1255
1187
  },
1256
- gitlab: {
1257
- id: "gitlab",
1258
- name: "GitLab",
1259
- envPrefix: "GITLAB",
1260
- clientIdVar: "GITLAB_CLIENT_ID",
1261
- clientSecretVar: "GITLAB_CLIENT_SECRET",
1188
+ notion: {
1189
+ id: "notion",
1190
+ name: "Notion",
1191
+ envPrefix: "NOTION",
1192
+ clientIdVar: "NOTION_CLIENT_ID",
1193
+ clientSecretVar: "NOTION_CLIENT_SECRET",
1262
1194
  popular: false,
1263
1195
  betterAuthConfig: {
1264
1196
  import: "",
1265
- clientSideProvider: '"gitlab"',
1266
- socialProvider: `gitlab({
1267
- clientId: process.env.GITLAB_CLIENT_ID!,
1268
- clientSecret: process.env.GITLAB_CLIENT_SECRET!,
1197
+ clientSideProvider: '"notion"',
1198
+ socialProvider: `notion({
1199
+ clientId: process.env.NOTION_CLIENT_ID!,
1200
+ clientSecret: process.env.NOTION_CLIENT_SECRET!,
1269
1201
  })`,
1270
1202
  scopes: []
1271
1203
  },
1272
1204
  env: [
1273
1205
  {
1274
- name: "GITLAB_CLIENT_ID",
1206
+ name: "NOTION_CLIENT_ID",
1275
1207
  type: "server",
1276
- description: "GitLab Application ID"
1208
+ description: "Notion OAuth Client ID"
1277
1209
  },
1278
1210
  {
1279
- name: "GITLAB_CLIENT_SECRET",
1211
+ name: "NOTION_CLIENT_SECRET",
1280
1212
  type: "server",
1281
- description: "GitLab Application Secret"
1213
+ description: "Notion OAuth Client Secret"
1282
1214
  }
1283
1215
  ],
1284
1216
  docs: {
1285
- provider: "https://gitlab.com/-/profile/applications",
1217
+ provider: "https://www.notion.so/my-integrations",
1286
1218
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1287
1219
  },
1288
1220
  requiresExtraConfig: false,
1289
- extraConfigNotes: "Optionally supports an issuer field for self-hosted GitLab instances.",
1221
+ extraConfigNotes: "",
1290
1222
  readme: {
1291
- title: "GitLab OAuth Setup",
1292
- content: `## GitLab OAuth Setup
1223
+ title: "Notion OAuth Setup",
1224
+ content: `## Notion OAuth Setup
1293
1225
 
1294
- 1. Create an application at https://gitlab.com/-/profile/applications
1295
- 2. Add redirect URI: \`http://localhost:3000/api/auth/callback/gitlab\` (update for production)
1296
- 3. Select the required scopes (read_user is recommended)
1297
- 4. Copy the Application ID and Secret to your \`.env\` file
1226
+ 1. Create an integration at https://www.notion.so/my-integrations
1227
+ 2. Configure OAuth settings and capabilities
1228
+ 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/notion\` (update for production)
1229
+ 4. Copy the OAuth Client ID and Secret to your \`.env\` file
1298
1230
 
1299
1231
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1300
1232
  }
1301
1233
  },
1302
- huggingface: {
1303
- id: "huggingface",
1304
- name: "Hugging Face",
1305
- envPrefix: "HUGGINGFACE",
1306
- clientIdVar: "HUGGINGFACE_CLIENT_ID",
1307
- clientSecretVar: "HUGGINGFACE_CLIENT_SECRET",
1234
+ paybin: {
1235
+ id: "paybin",
1236
+ name: "Paybin",
1237
+ envPrefix: "PAYBIN",
1238
+ clientIdVar: "PAYBIN_CLIENT_ID",
1239
+ clientSecretVar: "PAYBIN_CLIENT_SECRET",
1308
1240
  popular: false,
1309
1241
  betterAuthConfig: {
1310
1242
  import: "",
1311
- clientSideProvider: '"huggingface"',
1312
- socialProvider: `huggingface({
1313
- clientId: process.env.HUGGINGFACE_CLIENT_ID!,
1314
- clientSecret: process.env.HUGGINGFACE_CLIENT_SECRET!,
1243
+ clientSideProvider: '"paybin"',
1244
+ socialProvider: `paybin({
1245
+ clientId: process.env.PAYBIN_CLIENT_ID!,
1246
+ clientSecret: process.env.PAYBIN_CLIENT_SECRET!,
1315
1247
  })`,
1316
- scopes: ["email"]
1248
+ scopes: []
1317
1249
  },
1318
1250
  env: [
1319
1251
  {
1320
- name: "HUGGINGFACE_CLIENT_ID",
1252
+ name: "PAYBIN_CLIENT_ID",
1321
1253
  type: "server",
1322
- description: "Hugging Face OAuth Client ID"
1254
+ description: "Paybin OAuth Client ID"
1323
1255
  },
1324
1256
  {
1325
- name: "HUGGINGFACE_CLIENT_SECRET",
1257
+ name: "PAYBIN_CLIENT_SECRET",
1326
1258
  type: "server",
1327
- description: "Hugging Face OAuth Client Secret"
1259
+ description: "Paybin OAuth Client Secret"
1328
1260
  }
1329
1261
  ],
1330
1262
  docs: {
1331
- provider: "https://huggingface.co/settings/connected-applications",
1263
+ provider: "https://paybin.io/",
1332
1264
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1333
1265
  },
1334
- requiresExtraConfig: true,
1335
- extraConfigNotes: "You MUST include the email scope for Hugging Face.",
1266
+ requiresExtraConfig: false,
1267
+ extraConfigNotes: "Uses OpenID Connect scopes.",
1336
1268
  readme: {
1337
- title: "Hugging Face OAuth Setup",
1338
- content: `## Hugging Face OAuth Setup
1269
+ title: "Paybin OAuth Setup",
1270
+ content: `## Paybin OAuth Setup
1339
1271
 
1340
- 1. Create an OAuth app at https://huggingface.co/settings/connected-applications
1341
- 2. Set redirect URI to: \`http://localhost:3000/api/auth/callback/huggingface\` (update for production)
1342
- 3. Copy the Client ID and Client Secret to your \`.env\` file
1343
- 4. **Important**: Make sure to include the \`email\` scope in your configuration
1272
+ 1. Create an application at https://paybin.io/
1273
+ 2. Configure OAuth settings
1274
+ 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/paybin\` (update for production)
1275
+ 4. Copy the Client ID and Client Secret to your \`.env\` file
1344
1276
 
1345
1277
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1346
1278
  }
1347
1279
  },
1348
- kakao: {
1349
- id: "kakao",
1350
- name: "Kakao",
1351
- envPrefix: "KAKAO",
1352
- clientIdVar: "KAKAO_CLIENT_ID",
1353
- clientSecretVar: "KAKAO_CLIENT_SECRET",
1280
+ paypal: {
1281
+ id: "paypal",
1282
+ name: "PayPal",
1283
+ envPrefix: "PAYPAL",
1284
+ clientIdVar: "PAYPAL_CLIENT_ID",
1285
+ clientSecretVar: "PAYPAL_CLIENT_SECRET",
1354
1286
  popular: false,
1355
1287
  betterAuthConfig: {
1356
1288
  import: "",
1357
- clientSideProvider: '"kakao"',
1358
- socialProvider: `kakao({
1359
- clientId: process.env.KAKAO_CLIENT_ID!,
1360
- clientSecret: process.env.KAKAO_CLIENT_SECRET!,
1289
+ clientSideProvider: '"paypal"',
1290
+ socialProvider: `paypal({
1291
+ clientId: process.env.PAYPAL_CLIENT_ID!,
1292
+ clientSecret: process.env.PAYPAL_CLIENT_SECRET!,
1293
+ environment: process.env.PAYPAL_ENVIRONMENT || "sandbox",
1361
1294
  })`,
1362
1295
  scopes: []
1363
1296
  },
1364
1297
  env: [
1365
1298
  {
1366
- name: "KAKAO_CLIENT_ID",
1299
+ name: "PAYPAL_CLIENT_ID",
1367
1300
  type: "server",
1368
- description: "Kakao REST API Key"
1301
+ description: "PayPal REST API Client ID"
1369
1302
  },
1370
1303
  {
1371
- name: "KAKAO_CLIENT_SECRET",
1304
+ name: "PAYPAL_CLIENT_SECRET",
1372
1305
  type: "server",
1373
- description: "Kakao Client Secret"
1306
+ description: "PayPal REST API Secret"
1307
+ },
1308
+ {
1309
+ name: "PAYPAL_ENVIRONMENT",
1310
+ type: "server",
1311
+ description: 'PayPal environment: "sandbox" or "live" (default: sandbox)'
1374
1312
  }
1375
1313
  ],
1376
1314
  docs: {
1377
- provider: "https://developers.kakao.com/console/app",
1315
+ provider: "https://developer.paypal.com/dashboard/applications",
1378
1316
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1379
1317
  },
1380
- requiresExtraConfig: false,
1381
- extraConfigNotes: "",
1318
+ requiresExtraConfig: true,
1319
+ extraConfigNotes: "PayPal supports environment (sandbox/live) and requestShippingAddress options.",
1382
1320
  readme: {
1383
- title: "Kakao OAuth Setup",
1384
- content: `## Kakao OAuth Setup
1321
+ title: "PayPal OAuth Setup",
1322
+ content: `## PayPal OAuth Setup
1385
1323
 
1386
- 1. Create an application at https://developers.kakao.com/console/app
1387
- 2. Enable Kakao Login in the app settings
1388
- 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/kakao\` (update for production)
1389
- 4. Copy the REST API Key and Client Secret to your \`.env\` file
1324
+ 1. Create an app at https://developer.paypal.com/dashboard/applications
1325
+ 2. Configure OAuth settings in the app
1326
+ 3. Add return URL: \`http://localhost:3000/api/auth/callback/paypal\` (update for production)
1327
+ 4. Copy the Client ID and Secret to your \`.env\` file
1328
+ 5. Set PAYPAL_ENVIRONMENT to "sandbox" for testing or "live" for production
1390
1329
 
1391
1330
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1392
1331
  }
1393
1332
  },
1394
- kick: {
1395
- id: "kick",
1396
- name: "Kick",
1397
- envPrefix: "KICK",
1398
- clientIdVar: "KICK_CLIENT_ID",
1399
- clientSecretVar: "KICK_CLIENT_SECRET",
1333
+ polar: {
1334
+ id: "polar",
1335
+ name: "Polar",
1336
+ envPrefix: "POLAR",
1337
+ clientIdVar: "POLAR_CLIENT_ID",
1338
+ clientSecretVar: "POLAR_CLIENT_SECRET",
1400
1339
  popular: false,
1401
1340
  betterAuthConfig: {
1402
1341
  import: "",
1403
- clientSideProvider: '"kick"',
1404
- socialProvider: `kick({
1405
- clientId: process.env.KICK_CLIENT_ID!,
1406
- clientSecret: process.env.KICK_CLIENT_SECRET!,
1342
+ clientSideProvider: '"polar"',
1343
+ socialProvider: `polar({
1344
+ clientId: process.env.POLAR_CLIENT_ID!,
1345
+ clientSecret: process.env.POLAR_CLIENT_SECRET!,
1407
1346
  })`,
1408
1347
  scopes: []
1409
1348
  },
1410
1349
  env: [
1411
1350
  {
1412
- name: "KICK_CLIENT_ID",
1351
+ name: "POLAR_CLIENT_ID",
1413
1352
  type: "server",
1414
- description: "Kick OAuth Client ID"
1353
+ description: "Polar OAuth Client ID"
1415
1354
  },
1416
1355
  {
1417
- name: "KICK_CLIENT_SECRET",
1356
+ name: "POLAR_CLIENT_SECRET",
1418
1357
  type: "server",
1419
- description: "Kick OAuth Client Secret"
1358
+ description: "Polar OAuth Client Secret"
1420
1359
  }
1421
1360
  ],
1422
1361
  docs: {
1423
- provider: "https://dev.kick.com/",
1362
+ provider: "https://polar.sh/",
1424
1363
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1425
1364
  },
1426
1365
  requiresExtraConfig: false,
1427
- extraConfigNotes: "",
1366
+ extraConfigNotes: "Uses OpenID Connect scopes.",
1428
1367
  readme: {
1429
- title: "Kick OAuth Setup",
1430
- content: `## Kick OAuth Setup
1368
+ title: "Polar OAuth Setup",
1369
+ content: `## Polar OAuth Setup
1431
1370
 
1432
- 1. Create an application at https://dev.kick.com/
1371
+ 1. Create an OAuth application at https://polar.sh/
1433
1372
  2. Configure OAuth settings
1434
- 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/kick\` (update for production)
1373
+ 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/polar\` (update for production)
1435
1374
  4. Copy the Client ID and Client Secret to your \`.env\` file
1436
1375
 
1437
1376
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1438
1377
  }
1439
1378
  },
1440
- line: {
1441
- id: "line",
1442
- name: "LINE",
1443
- envPrefix: "LINE",
1444
- clientIdVar: "LINE_CLIENT_ID",
1445
- clientSecretVar: "LINE_CLIENT_SECRET",
1379
+ reddit: {
1380
+ id: "reddit",
1381
+ name: "Reddit",
1382
+ envPrefix: "REDDIT",
1383
+ clientIdVar: "REDDIT_CLIENT_ID",
1384
+ clientSecretVar: "REDDIT_CLIENT_SECRET",
1446
1385
  popular: false,
1447
1386
  betterAuthConfig: {
1448
1387
  import: "",
1449
- clientSideProvider: '"line"',
1450
- socialProvider: `line({
1451
- clientId: process.env.LINE_CLIENT_ID!,
1452
- clientSecret: process.env.LINE_CLIENT_SECRET!,
1388
+ clientSideProvider: '"reddit"',
1389
+ socialProvider: `reddit({
1390
+ clientId: process.env.REDDIT_CLIENT_ID!,
1391
+ clientSecret: process.env.REDDIT_CLIENT_SECRET!,
1453
1392
  })`,
1454
1393
  scopes: []
1455
1394
  },
1456
1395
  env: [
1457
1396
  {
1458
- name: "LINE_CLIENT_ID",
1397
+ name: "REDDIT_CLIENT_ID",
1459
1398
  type: "server",
1460
- description: "LINE Channel ID"
1399
+ description: "Reddit App Client ID"
1461
1400
  },
1462
1401
  {
1463
- name: "LINE_CLIENT_SECRET",
1402
+ name: "REDDIT_CLIENT_SECRET",
1464
1403
  type: "server",
1465
- description: "LINE Channel Secret"
1404
+ description: "Reddit App Client Secret"
1466
1405
  }
1467
1406
  ],
1468
1407
  docs: {
1469
- provider: "https://developers.line.biz/console/",
1408
+ provider: "https://www.reddit.com/prefs/apps",
1470
1409
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1471
1410
  },
1472
1411
  requiresExtraConfig: false,
1473
- extraConfigNotes: "Supports multi-channel configuration.",
1412
+ extraConfigNotes: "Supports duration and scope fields for custom access.",
1474
1413
  readme: {
1475
- title: "LINE OAuth Setup",
1476
- content: `## LINE OAuth Setup
1414
+ title: "Reddit OAuth Setup",
1415
+ content: `## Reddit OAuth Setup
1477
1416
 
1478
- 1. Create a channel at https://developers.line.biz/console/
1479
- 2. Enable LINE Login
1480
- 3. Add callback URL: \`http://localhost:3000/api/auth/callback/line\` (update for production)
1481
- 4. Copy the Channel ID and Channel Secret to your \`.env\` file
1417
+ 1. Create an app at https://www.reddit.com/prefs/apps
1418
+ 2. Choose "web app" as the app type
1419
+ 3. Set redirect URI to: \`http://localhost:3000/api/auth/callback/reddit\` (update for production)
1420
+ 4. Copy the Client ID (under app name) and Client Secret to your \`.env\` file
1482
1421
 
1483
1422
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1484
1423
  }
1485
1424
  },
1486
- linear: {
1487
- id: "linear",
1488
- name: "Linear",
1489
- envPrefix: "LINEAR",
1490
- clientIdVar: "LINEAR_CLIENT_ID",
1491
- clientSecretVar: "LINEAR_CLIENT_SECRET",
1425
+ roblox: {
1426
+ id: "roblox",
1427
+ name: "Roblox",
1428
+ envPrefix: "ROBLOX",
1429
+ clientIdVar: "ROBLOX_CLIENT_ID",
1430
+ clientSecretVar: "ROBLOX_CLIENT_SECRET",
1492
1431
  popular: false,
1493
1432
  betterAuthConfig: {
1494
1433
  import: "",
1495
- clientSideProvider: '"linear"',
1496
- socialProvider: `linear({
1497
- clientId: process.env.LINEAR_CLIENT_ID!,
1498
- clientSecret: process.env.LINEAR_CLIENT_SECRET!,
1434
+ clientSideProvider: '"roblox"',
1435
+ socialProvider: `roblox({
1436
+ clientId: process.env.ROBLOX_CLIENT_ID!,
1437
+ clientSecret: process.env.ROBLOX_CLIENT_SECRET!,
1499
1438
  })`,
1500
1439
  scopes: []
1501
1440
  },
1502
1441
  env: [
1503
1442
  {
1504
- name: "LINEAR_CLIENT_ID",
1443
+ name: "ROBLOX_CLIENT_ID",
1505
1444
  type: "server",
1506
- description: "Linear OAuth Client ID"
1445
+ description: "Roblox OAuth Client ID"
1507
1446
  },
1508
1447
  {
1509
- name: "LINEAR_CLIENT_SECRET",
1448
+ name: "ROBLOX_CLIENT_SECRET",
1510
1449
  type: "server",
1511
- description: "Linear OAuth Client Secret"
1450
+ description: "Roblox OAuth Client Secret"
1512
1451
  }
1513
1452
  ],
1514
1453
  docs: {
1515
- provider: "https://linear.app/settings/api",
1454
+ provider: "https://create.roblox.com/credentials",
1516
1455
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1517
1456
  },
1518
- requiresExtraConfig: false,
1519
- extraConfigNotes: "Supports custom scope options.",
1457
+ requiresExtraConfig: true,
1458
+ extraConfigNotes: "Note: Roblox OAuth does not provide user email addresses.",
1520
1459
  readme: {
1521
- title: "Linear OAuth Setup",
1522
- content: `## Linear OAuth Setup
1460
+ title: "Roblox OAuth Setup",
1461
+ content: `## Roblox OAuth Setup
1523
1462
 
1524
- 1. Create an OAuth application at https://linear.app/settings/api
1525
- 2. Add redirect URL: \`http://localhost:3000/api/auth/callback/linear\` (update for production)
1526
- 3. Copy the Client ID and Client Secret to your \`.env\` file
1463
+ 1. Create OAuth credentials at https://create.roblox.com/credentials
1464
+ 2. Configure OAuth 2.0 settings
1465
+ 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/roblox\` (update for production)
1466
+ 4. Copy the Client ID and Client Secret to your \`.env\` file
1467
+
1468
+ **Note**: Roblox does not provide email addresses through OAuth.
1527
1469
 
1528
1470
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1529
1471
  }
1530
1472
  },
1531
- naver: {
1532
- id: "naver",
1533
- name: "Naver",
1534
- envPrefix: "NAVER",
1535
- clientIdVar: "NAVER_CLIENT_ID",
1536
- clientSecretVar: "NAVER_CLIENT_SECRET",
1473
+ salesforce: {
1474
+ id: "salesforce",
1475
+ name: "Salesforce",
1476
+ envPrefix: "SALESFORCE",
1477
+ clientIdVar: "SALESFORCE_CLIENT_ID",
1478
+ clientSecretVar: "SALESFORCE_CLIENT_SECRET",
1537
1479
  popular: false,
1538
1480
  betterAuthConfig: {
1539
1481
  import: "",
1540
- clientSideProvider: '"naver"',
1541
- socialProvider: `naver({
1542
- clientId: process.env.NAVER_CLIENT_ID!,
1543
- clientSecret: process.env.NAVER_CLIENT_SECRET!,
1482
+ clientSideProvider: '"salesforce"',
1483
+ socialProvider: `salesforce({
1484
+ clientId: process.env.SALESFORCE_CLIENT_ID!,
1485
+ clientSecret: process.env.SALESFORCE_CLIENT_SECRET!,
1486
+ environment: process.env.SALESFORCE_ENVIRONMENT || "login",
1544
1487
  })`,
1545
1488
  scopes: []
1546
1489
  },
1547
1490
  env: [
1548
1491
  {
1549
- name: "NAVER_CLIENT_ID",
1492
+ name: "SALESFORCE_CLIENT_ID",
1550
1493
  type: "server",
1551
- description: "Naver Client ID"
1494
+ description: "Salesforce Connected App Consumer Key"
1552
1495
  },
1553
1496
  {
1554
- name: "NAVER_CLIENT_SECRET",
1497
+ name: "SALESFORCE_CLIENT_SECRET",
1555
1498
  type: "server",
1556
- description: "Naver Client Secret"
1499
+ description: "Salesforce Connected App Consumer Secret"
1500
+ },
1501
+ {
1502
+ name: "SALESFORCE_ENVIRONMENT",
1503
+ type: "server",
1504
+ description: 'Salesforce environment: "login" (production) or "test" (sandbox) (default: login)'
1557
1505
  }
1558
1506
  ],
1559
1507
  docs: {
1560
- provider: "https://developers.naver.com/apps/",
1508
+ provider: "https://developer.salesforce.com/",
1561
1509
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1562
1510
  },
1563
- requiresExtraConfig: false,
1564
- extraConfigNotes: "",
1511
+ requiresExtraConfig: true,
1512
+ extraConfigNotes: 'Salesforce supports environment field: "login" for production, "test" for sandbox.',
1565
1513
  readme: {
1566
- title: "Naver OAuth Setup",
1567
- content: `## Naver OAuth Setup
1514
+ title: "Salesforce OAuth Setup",
1515
+ content: `## Salesforce OAuth Setup
1568
1516
 
1569
- 1. Register an application at https://developers.naver.com/apps/
1570
- 2. Configure Login API settings
1571
- 3. Add callback URL: \`http://localhost:3000/api/auth/callback/naver\` (update for production)
1572
- 4. Copy the Client ID and Client Secret to your \`.env\` file
1517
+ 1. Create a Connected App in Salesforce Setup
1518
+ 2. Enable OAuth Settings
1519
+ 3. Add callback URL: \`http://localhost:3000/api/auth/callback/salesforce\` (update for production)
1520
+ 4. Copy the Consumer Key and Consumer Secret to your \`.env\` file
1521
+ 5. Set SALESFORCE_ENVIRONMENT to "login" for production or "test" for sandbox
1573
1522
 
1574
1523
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1575
1524
  }
1576
1525
  },
1577
- notion: {
1578
- id: "notion",
1579
- name: "Notion",
1580
- envPrefix: "NOTION",
1581
- clientIdVar: "NOTION_CLIENT_ID",
1582
- clientSecretVar: "NOTION_CLIENT_SECRET",
1526
+ slack: {
1527
+ id: "slack",
1528
+ name: "Slack",
1529
+ envPrefix: "SLACK",
1530
+ clientIdVar: "SLACK_CLIENT_ID",
1531
+ clientSecretVar: "SLACK_CLIENT_SECRET",
1583
1532
  popular: false,
1584
1533
  betterAuthConfig: {
1585
1534
  import: "",
1586
- clientSideProvider: '"notion"',
1587
- socialProvider: `notion({
1588
- clientId: process.env.NOTION_CLIENT_ID!,
1589
- clientSecret: process.env.NOTION_CLIENT_SECRET!,
1535
+ clientSideProvider: '"slack"',
1536
+ socialProvider: `slack({
1537
+ clientId: process.env.SLACK_CLIENT_ID!,
1538
+ clientSecret: process.env.SLACK_CLIENT_SECRET!,
1590
1539
  })`,
1591
1540
  scopes: []
1592
1541
  },
1593
1542
  env: [
1594
1543
  {
1595
- name: "NOTION_CLIENT_ID",
1544
+ name: "SLACK_CLIENT_ID",
1596
1545
  type: "server",
1597
- description: "Notion OAuth Client ID"
1546
+ description: "Slack App Client ID"
1598
1547
  },
1599
1548
  {
1600
- name: "NOTION_CLIENT_SECRET",
1549
+ name: "SLACK_CLIENT_SECRET",
1601
1550
  type: "server",
1602
- description: "Notion OAuth Client Secret"
1551
+ description: "Slack App Client Secret"
1603
1552
  }
1604
1553
  ],
1605
1554
  docs: {
1606
- provider: "https://www.notion.so/my-integrations",
1555
+ provider: "https://api.slack.com/apps",
1607
1556
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1608
1557
  },
1609
1558
  requiresExtraConfig: false,
1610
- extraConfigNotes: "",
1559
+ extraConfigNotes: "Supports optional team field for workspace restrictions.",
1611
1560
  readme: {
1612
- title: "Notion OAuth Setup",
1613
- content: `## Notion OAuth Setup
1561
+ title: "Slack OAuth Setup",
1562
+ content: `## Slack OAuth Setup
1614
1563
 
1615
- 1. Create an integration at https://www.notion.so/my-integrations
1616
- 2. Configure OAuth settings and capabilities
1617
- 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/notion\` (update for production)
1618
- 4. Copy the OAuth Client ID and Secret to your \`.env\` file
1564
+ 1. Create a Slack app at https://api.slack.com/apps
1565
+ 2. Add OAuth & Permissions and configure redirect URLs
1566
+ 3. Add redirect URL: \`http://localhost:3000/api/auth/callback/slack\` (update for production)
1567
+ 4. Copy the Client ID and Client Secret to your \`.env\` file
1619
1568
 
1620
1569
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1621
1570
  }
1622
1571
  },
1623
- paybin: {
1624
- id: "paybin",
1625
- name: "Paybin",
1626
- envPrefix: "PAYBIN",
1627
- clientIdVar: "PAYBIN_CLIENT_ID",
1628
- clientSecretVar: "PAYBIN_CLIENT_SECRET",
1572
+ tiktok: {
1573
+ id: "tiktok",
1574
+ name: "TikTok",
1575
+ envPrefix: "TIKTOK",
1576
+ clientIdVar: "TIKTOK_CLIENT_KEY",
1577
+ clientSecretVar: "TIKTOK_CLIENT_SECRET",
1629
1578
  popular: false,
1630
1579
  betterAuthConfig: {
1631
1580
  import: "",
1632
- clientSideProvider: '"paybin"',
1633
- socialProvider: `paybin({
1634
- clientId: process.env.PAYBIN_CLIENT_ID!,
1635
- clientSecret: process.env.PAYBIN_CLIENT_SECRET!,
1581
+ clientSideProvider: '"tiktok"',
1582
+ socialProvider: `tiktok({
1583
+ clientKey: process.env.TIKTOK_CLIENT_KEY!,
1584
+ clientSecret: process.env.TIKTOK_CLIENT_SECRET!,
1636
1585
  })`,
1637
1586
  scopes: []
1638
1587
  },
1639
1588
  env: [
1640
1589
  {
1641
- name: "PAYBIN_CLIENT_ID",
1590
+ name: "TIKTOK_CLIENT_KEY",
1642
1591
  type: "server",
1643
- description: "Paybin OAuth Client ID"
1592
+ description: "TikTok Client Key (not Client ID)"
1644
1593
  },
1645
1594
  {
1646
- name: "PAYBIN_CLIENT_SECRET",
1595
+ name: "TIKTOK_CLIENT_SECRET",
1647
1596
  type: "server",
1648
- description: "Paybin OAuth Client Secret"
1597
+ description: "TikTok Client Secret"
1649
1598
  }
1650
1599
  ],
1651
1600
  docs: {
1652
- provider: "https://paybin.io/",
1653
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
1654
- },
1655
- requiresExtraConfig: false,
1656
- extraConfigNotes: "Uses OpenID Connect scopes.",
1657
- readme: {
1658
- title: "Paybin OAuth Setup",
1659
- content: `## Paybin OAuth Setup
1660
-
1661
- 1. Create an application at https://paybin.io/
1662
- 2. Configure OAuth settings
1663
- 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/paybin\` (update for production)
1664
- 4. Copy the Client ID and Client Secret to your \`.env\` file
1665
-
1666
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1667
- }
1668
- },
1669
- paypal: {
1670
- id: "paypal",
1671
- name: "PayPal",
1672
- envPrefix: "PAYPAL",
1673
- clientIdVar: "PAYPAL_CLIENT_ID",
1674
- clientSecretVar: "PAYPAL_CLIENT_SECRET",
1675
- popular: false,
1676
- betterAuthConfig: {
1677
- import: "",
1678
- clientSideProvider: '"paypal"',
1679
- socialProvider: `paypal({
1680
- clientId: process.env.PAYPAL_CLIENT_ID!,
1681
- clientSecret: process.env.PAYPAL_CLIENT_SECRET!,
1682
- environment: process.env.PAYPAL_ENVIRONMENT || "sandbox",
1683
- })`,
1684
- scopes: []
1685
- },
1686
- env: [
1687
- {
1688
- name: "PAYPAL_CLIENT_ID",
1689
- type: "server",
1690
- description: "PayPal REST API Client ID"
1691
- },
1692
- {
1693
- name: "PAYPAL_CLIENT_SECRET",
1694
- type: "server",
1695
- description: "PayPal REST API Secret"
1696
- },
1697
- {
1698
- name: "PAYPAL_ENVIRONMENT",
1699
- type: "server",
1700
- description: 'PayPal environment: "sandbox" or "live" (default: sandbox)'
1701
- }
1702
- ],
1703
- docs: {
1704
- provider: "https://developer.paypal.com/dashboard/applications",
1601
+ provider: "https://developers.tiktok.com/",
1705
1602
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1706
1603
  },
1707
1604
  requiresExtraConfig: true,
1708
- extraConfigNotes: "PayPal supports environment (sandbox/live) and requestShippingAddress options.",
1605
+ extraConfigNotes: "TikTok uses clientKey instead of clientId. Make sure to use the correct field name.",
1709
1606
  readme: {
1710
- title: "PayPal OAuth Setup",
1711
- content: `## PayPal OAuth Setup
1607
+ title: "TikTok OAuth Setup",
1608
+ content: `## TikTok OAuth Setup
1712
1609
 
1713
- 1. Create an app at https://developer.paypal.com/dashboard/applications
1714
- 2. Configure OAuth settings in the app
1715
- 3. Add return URL: \`http://localhost:3000/api/auth/callback/paypal\` (update for production)
1716
- 4. Copy the Client ID and Secret to your \`.env\` file
1717
- 5. Set PAYPAL_ENVIRONMENT to "sandbox" for testing or "live" for production
1610
+ 1. Register an app at https://developers.tiktok.com/
1611
+ 2. Configure Login Kit
1612
+ 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/tiktok\` (update for production)
1613
+ 4. Copy the Client Key (not Client ID) and Client Secret to your \`.env\` file
1614
+
1615
+ **Important**: TikTok uses clientKey instead of clientId.
1718
1616
 
1719
1617
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1720
1618
  }
1721
1619
  },
1722
- polar: {
1723
- id: "polar",
1724
- name: "Polar",
1725
- envPrefix: "POLAR",
1726
- clientIdVar: "POLAR_CLIENT_ID",
1727
- clientSecretVar: "POLAR_CLIENT_SECRET",
1620
+ vercel: {
1621
+ id: "vercel",
1622
+ name: "Vercel",
1623
+ envPrefix: "VERCEL",
1624
+ clientIdVar: "VERCEL_CLIENT_ID",
1625
+ clientSecretVar: "VERCEL_CLIENT_SECRET",
1728
1626
  popular: false,
1729
1627
  betterAuthConfig: {
1730
1628
  import: "",
1731
- clientSideProvider: '"polar"',
1732
- socialProvider: `polar({
1733
- clientId: process.env.POLAR_CLIENT_ID!,
1734
- clientSecret: process.env.POLAR_CLIENT_SECRET!,
1629
+ clientSideProvider: '"vercel"',
1630
+ socialProvider: `vercel({
1631
+ clientId: process.env.VERCEL_CLIENT_ID!,
1632
+ clientSecret: process.env.VERCEL_CLIENT_SECRET!,
1735
1633
  })`,
1736
1634
  scopes: []
1737
1635
  },
1738
1636
  env: [
1739
1637
  {
1740
- name: "POLAR_CLIENT_ID",
1638
+ name: "VERCEL_CLIENT_ID",
1741
1639
  type: "server",
1742
- description: "Polar OAuth Client ID"
1640
+ description: "Vercel OAuth Client ID"
1743
1641
  },
1744
1642
  {
1745
- name: "POLAR_CLIENT_SECRET",
1643
+ name: "VERCEL_CLIENT_SECRET",
1746
1644
  type: "server",
1747
- description: "Polar OAuth Client Secret"
1645
+ description: "Vercel OAuth Client Secret"
1748
1646
  }
1749
1647
  ],
1750
1648
  docs: {
1751
- provider: "https://polar.sh/",
1649
+ provider: "https://vercel.com/account/integrations",
1752
1650
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1753
1651
  },
1754
1652
  requiresExtraConfig: false,
1755
- extraConfigNotes: "Uses OpenID Connect scopes.",
1653
+ extraConfigNotes: "Uses PKCE for enhanced security.",
1756
1654
  readme: {
1757
- title: "Polar OAuth Setup",
1758
- content: `## Polar OAuth Setup
1655
+ title: "Vercel OAuth Setup",
1656
+ content: `## Vercel OAuth Setup
1759
1657
 
1760
- 1. Create an OAuth application at https://polar.sh/
1658
+ 1. Create an integration at https://vercel.com/account/integrations
1761
1659
  2. Configure OAuth settings
1762
- 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/polar\` (update for production)
1660
+ 3. Add redirect URL: \`http://localhost:3000/api/auth/callback/vercel\` (update for production)
1763
1661
  4. Copy the Client ID and Client Secret to your \`.env\` file
1764
1662
 
1765
1663
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1766
1664
  }
1767
1665
  },
1768
- reddit: {
1769
- id: "reddit",
1770
- name: "Reddit",
1771
- envPrefix: "REDDIT",
1772
- clientIdVar: "REDDIT_CLIENT_ID",
1773
- clientSecretVar: "REDDIT_CLIENT_SECRET",
1666
+ vk: {
1667
+ id: "vk",
1668
+ name: "VK",
1669
+ envPrefix: "VK",
1670
+ clientIdVar: "VK_CLIENT_ID",
1671
+ clientSecretVar: "VK_CLIENT_SECRET",
1774
1672
  popular: false,
1775
1673
  betterAuthConfig: {
1776
1674
  import: "",
1777
- clientSideProvider: '"reddit"',
1778
- socialProvider: `reddit({
1779
- clientId: process.env.REDDIT_CLIENT_ID!,
1780
- clientSecret: process.env.REDDIT_CLIENT_SECRET!,
1675
+ clientSideProvider: '"vk"',
1676
+ socialProvider: `vk({
1677
+ clientId: process.env.VK_CLIENT_ID!,
1678
+ clientSecret: process.env.VK_CLIENT_SECRET!,
1781
1679
  })`,
1782
1680
  scopes: []
1783
1681
  },
1784
1682
  env: [
1785
1683
  {
1786
- name: "REDDIT_CLIENT_ID",
1684
+ name: "VK_CLIENT_ID",
1787
1685
  type: "server",
1788
- description: "Reddit App Client ID"
1686
+ description: "VK Application ID"
1789
1687
  },
1790
1688
  {
1791
- name: "REDDIT_CLIENT_SECRET",
1689
+ name: "VK_CLIENT_SECRET",
1792
1690
  type: "server",
1793
- description: "Reddit App Client Secret"
1691
+ description: "VK Secure Key"
1794
1692
  }
1795
1693
  ],
1796
1694
  docs: {
1797
- provider: "https://www.reddit.com/prefs/apps",
1695
+ provider: "https://vk.com/apps?act=manage",
1798
1696
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1799
1697
  },
1800
1698
  requiresExtraConfig: false,
1801
- extraConfigNotes: "Supports duration and scope fields for custom access.",
1699
+ extraConfigNotes: "",
1802
1700
  readme: {
1803
- title: "Reddit OAuth Setup",
1804
- content: `## Reddit OAuth Setup
1701
+ title: "VK OAuth Setup",
1702
+ content: `## VK OAuth Setup
1805
1703
 
1806
- 1. Create an app at https://www.reddit.com/prefs/apps
1807
- 2. Choose "web app" as the app type
1808
- 3. Set redirect URI to: \`http://localhost:3000/api/auth/callback/reddit\` (update for production)
1809
- 4. Copy the Client ID (under app name) and Client Secret to your \`.env\` file
1704
+ 1. Create an app at https://vk.com/apps?act=manage
1705
+ 2. Configure OAuth settings in the app
1706
+ 3. Add authorized redirect URI: \`http://localhost:3000/api/auth/callback/vk\` (update for production)
1707
+ 4. Copy the Application ID and Secure Key to your \`.env\` file
1810
1708
 
1811
1709
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1812
1710
  }
1813
1711
  },
1814
- roblox: {
1815
- id: "roblox",
1816
- name: "Roblox",
1817
- envPrefix: "ROBLOX",
1818
- clientIdVar: "ROBLOX_CLIENT_ID",
1819
- clientSecretVar: "ROBLOX_CLIENT_SECRET",
1712
+ zoom: {
1713
+ id: "zoom",
1714
+ name: "Zoom",
1715
+ envPrefix: "ZOOM",
1716
+ clientIdVar: "ZOOM_CLIENT_ID",
1717
+ clientSecretVar: "ZOOM_CLIENT_SECRET",
1820
1718
  popular: false,
1821
1719
  betterAuthConfig: {
1822
1720
  import: "",
1823
- clientSideProvider: '"roblox"',
1824
- socialProvider: `roblox({
1825
- clientId: process.env.ROBLOX_CLIENT_ID!,
1826
- clientSecret: process.env.ROBLOX_CLIENT_SECRET!,
1721
+ clientSideProvider: '"zoom"',
1722
+ socialProvider: `zoom({
1723
+ clientId: process.env.ZOOM_CLIENT_ID!,
1724
+ clientSecret: process.env.ZOOM_CLIENT_SECRET!,
1827
1725
  })`,
1828
- scopes: []
1726
+ scopes: ["user:read:user"]
1829
1727
  },
1830
1728
  env: [
1831
1729
  {
1832
- name: "ROBLOX_CLIENT_ID",
1730
+ name: "ZOOM_CLIENT_ID",
1833
1731
  type: "server",
1834
- description: "Roblox OAuth Client ID"
1732
+ description: "Zoom OAuth Client ID"
1835
1733
  },
1836
1734
  {
1837
- name: "ROBLOX_CLIENT_SECRET",
1735
+ name: "ZOOM_CLIENT_SECRET",
1838
1736
  type: "server",
1839
- description: "Roblox OAuth Client Secret"
1737
+ description: "Zoom OAuth Client Secret"
1840
1738
  }
1841
1739
  ],
1842
1740
  docs: {
1843
- provider: "https://create.roblox.com/credentials",
1741
+ provider: "https://marketplace.zoom.us/",
1844
1742
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1845
1743
  },
1846
1744
  requiresExtraConfig: true,
1847
- extraConfigNotes: "Note: Roblox OAuth does not provide user email addresses.",
1745
+ extraConfigNotes: "You MUST include the user:read:user scope for Zoom.",
1848
1746
  readme: {
1849
- title: "Roblox OAuth Setup",
1850
- content: `## Roblox OAuth Setup
1747
+ title: "Zoom OAuth Setup",
1748
+ content: `## Zoom OAuth Setup
1851
1749
 
1852
- 1. Create OAuth credentials at https://create.roblox.com/credentials
1853
- 2. Configure OAuth 2.0 settings
1854
- 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/roblox\` (update for production)
1750
+ 1. Create an app at https://marketplace.zoom.us/
1751
+ 2. Choose OAuth as the app type
1752
+ 3. Add redirect URL: \`http://localhost:3000/api/auth/callback/zoom\` (update for production)
1855
1753
  4. Copy the Client ID and Client Secret to your \`.env\` file
1856
-
1857
- **Note**: Roblox does not provide email addresses through OAuth.
1858
-
1859
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1860
- }
1861
- },
1862
- salesforce: {
1863
- id: "salesforce",
1864
- name: "Salesforce",
1865
- envPrefix: "SALESFORCE",
1866
- clientIdVar: "SALESFORCE_CLIENT_ID",
1867
- clientSecretVar: "SALESFORCE_CLIENT_SECRET",
1868
- popular: false,
1869
- betterAuthConfig: {
1870
- import: "",
1871
- clientSideProvider: '"salesforce"',
1872
- socialProvider: `salesforce({
1873
- clientId: process.env.SALESFORCE_CLIENT_ID!,
1874
- clientSecret: process.env.SALESFORCE_CLIENT_SECRET!,
1875
- environment: process.env.SALESFORCE_ENVIRONMENT || "login",
1876
- })`,
1877
- scopes: []
1878
- },
1879
- env: [
1880
- {
1881
- name: "SALESFORCE_CLIENT_ID",
1882
- type: "server",
1883
- description: "Salesforce Connected App Consumer Key"
1884
- },
1885
- {
1886
- name: "SALESFORCE_CLIENT_SECRET",
1887
- type: "server",
1888
- description: "Salesforce Connected App Consumer Secret"
1889
- },
1890
- {
1891
- name: "SALESFORCE_ENVIRONMENT",
1892
- type: "server",
1893
- description: 'Salesforce environment: "login" (production) or "test" (sandbox) (default: login)'
1894
- }
1895
- ],
1896
- docs: {
1897
- provider: "https://developer.salesforce.com/",
1898
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
1899
- },
1900
- requiresExtraConfig: true,
1901
- extraConfigNotes: 'Salesforce supports environment field: "login" for production, "test" for sandbox.',
1902
- readme: {
1903
- title: "Salesforce OAuth Setup",
1904
- content: `## Salesforce OAuth Setup
1905
-
1906
- 1. Create a Connected App in Salesforce Setup
1907
- 2. Enable OAuth Settings
1908
- 3. Add callback URL: \`http://localhost:3000/api/auth/callback/salesforce\` (update for production)
1909
- 4. Copy the Consumer Key and Consumer Secret to your \`.env\` file
1910
- 5. Set SALESFORCE_ENVIRONMENT to "login" for production or "test" for sandbox
1754
+ 5. **Important**: Make sure to include the \`user:read:user\` scope
1911
1755
 
1912
1756
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1913
1757
  }
1914
- },
1915
- slack: {
1916
- id: "slack",
1917
- name: "Slack",
1918
- envPrefix: "SLACK",
1919
- clientIdVar: "SLACK_CLIENT_ID",
1920
- clientSecretVar: "SLACK_CLIENT_SECRET",
1921
- popular: false,
1922
- betterAuthConfig: {
1923
- import: "",
1924
- clientSideProvider: '"slack"',
1925
- socialProvider: `slack({
1926
- clientId: process.env.SLACK_CLIENT_ID!,
1927
- clientSecret: process.env.SLACK_CLIENT_SECRET!,
1928
- })`,
1929
- scopes: []
1930
- },
1931
- env: [
1932
- {
1933
- name: "SLACK_CLIENT_ID",
1934
- type: "server",
1935
- description: "Slack App Client ID"
1936
- },
1937
- {
1938
- name: "SLACK_CLIENT_SECRET",
1939
- type: "server",
1940
- description: "Slack App Client Secret"
1941
- }
1942
- ],
1943
- docs: {
1944
- provider: "https://api.slack.com/apps",
1945
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
1946
- },
1947
- requiresExtraConfig: false,
1948
- extraConfigNotes: "Supports optional team field for workspace restrictions.",
1949
- readme: {
1950
- title: "Slack OAuth Setup",
1951
- content: `## Slack OAuth Setup
1952
-
1953
- 1. Create a Slack app at https://api.slack.com/apps
1954
- 2. Add OAuth & Permissions and configure redirect URLs
1955
- 3. Add redirect URL: \`http://localhost:3000/api/auth/callback/slack\` (update for production)
1956
- 4. Copy the Client ID and Client Secret to your \`.env\` file
1758
+ }
1759
+ };
1760
+ function getProvider(id) {
1761
+ return OAUTH_PROVIDERS[id];
1762
+ }
1763
+ function getPopularProviders() {
1764
+ return Object.values(OAUTH_PROVIDERS).filter(
1765
+ (provider) => provider.popular === true
1766
+ );
1767
+ }
1768
+ function getAdditionalProviders() {
1769
+ return Object.values(OAUTH_PROVIDERS).filter(
1770
+ (provider) => provider.popular !== true
1771
+ );
1772
+ }
1957
1773
 
1958
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1774
+ // src/installers/string-utils.ts
1775
+ function detectIndentation(line) {
1776
+ const match = line.match(/^(\s*)/);
1777
+ return match ? match[1] : "";
1778
+ }
1779
+ async function replacePlaceholder(filePath, placeholder, content, options) {
1780
+ const fileContent = await fs4.readFile(filePath, "utf-8");
1781
+ if (!fileContent.includes(placeholder)) {
1782
+ if (options?.graceful) {
1783
+ console.warn(
1784
+ `Warning: Placeholder "${placeholder}" not found in file: ${filePath}. Skipping replacement.`
1785
+ );
1786
+ return;
1959
1787
  }
1960
- },
1961
- tiktok: {
1962
- id: "tiktok",
1963
- name: "TikTok",
1964
- envPrefix: "TIKTOK",
1965
- clientIdVar: "TIKTOK_CLIENT_KEY",
1966
- clientSecretVar: "TIKTOK_CLIENT_SECRET",
1967
- popular: false,
1968
- betterAuthConfig: {
1969
- import: "",
1970
- clientSideProvider: '"tiktok"',
1971
- socialProvider: `tiktok({
1972
- clientKey: process.env.TIKTOK_CLIENT_KEY!,
1973
- clientSecret: process.env.TIKTOK_CLIENT_SECRET!,
1974
- })`,
1975
- scopes: []
1976
- },
1977
- env: [
1978
- {
1979
- name: "TIKTOK_CLIENT_KEY",
1980
- type: "server",
1981
- description: "TikTok Client Key (not Client ID)"
1982
- },
1983
- {
1984
- name: "TIKTOK_CLIENT_SECRET",
1985
- type: "server",
1986
- description: "TikTok Client Secret"
1788
+ throw new Error(
1789
+ `Placeholder "${placeholder}" not found in file: ${filePath}`
1790
+ );
1791
+ }
1792
+ if (options?.inline) {
1793
+ const updatedContent = fileContent.replace(placeholder, content);
1794
+ await fs4.writeFile(filePath, updatedContent, "utf-8");
1795
+ return;
1796
+ }
1797
+ const lines = fileContent.split("\n");
1798
+ const updatedLines = [];
1799
+ for (const line of lines) {
1800
+ if (line.includes(placeholder)) {
1801
+ if (content === "" || content.startsWith("__REMOVE_")) {
1802
+ continue;
1987
1803
  }
1988
- ],
1989
- docs: {
1990
- provider: "https://developers.tiktok.com/",
1991
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
1992
- },
1993
- requiresExtraConfig: true,
1994
- extraConfigNotes: "TikTok uses clientKey instead of clientId. Make sure to use the correct field name.",
1995
- readme: {
1996
- title: "TikTok OAuth Setup",
1997
- content: `## TikTok OAuth Setup
1998
-
1999
- 1. Register an app at https://developers.tiktok.com/
2000
- 2. Configure Login Kit
2001
- 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/tiktok\` (update for production)
2002
- 4. Copy the Client Key (not Client ID) and Client Secret to your \`.env\` file
1804
+ const indentation = detectIndentation(line);
1805
+ const indentedContent = content.split("\n").map((contentLine, index) => {
1806
+ if (index === 0) {
1807
+ return indentation + contentLine;
1808
+ }
1809
+ return contentLine ? indentation + contentLine : "";
1810
+ }).join("\n");
1811
+ updatedLines.push(indentedContent);
1812
+ } else {
1813
+ updatedLines.push(line);
1814
+ }
1815
+ }
1816
+ await fs4.writeFile(filePath, updatedLines.join("\n"), "utf-8");
1817
+ }
1818
+ function generateCredentialsValue(enabled) {
1819
+ return `credentials={${enabled}}`;
1820
+ }
1821
+ function generateAuthProvidersBlock(oauthProviders, emailPasswordEnabled) {
1822
+ const parts = [];
1823
+ parts.push(`emailAndPassword: {
1824
+ enabled: ${emailPasswordEnabled}
1825
+ },`);
1826
+ if (oauthProviders.length > 0) {
1827
+ const providersObject = oauthProviders.map((providerId) => {
1828
+ const provider = getProvider(providerId);
1829
+ if (!provider) {
1830
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
1831
+ }
1832
+ const configLines = [
1833
+ `clientId: process.env.${provider.envPrefix}_CLIENT_ID!,`,
1834
+ `clientSecret: process.env.${provider.envPrefix}_CLIENT_SECRET!,`
1835
+ ];
1836
+ if (providerId === "figma") {
1837
+ configLines.push(`clientKey: process.env.FIGMA_CLIENT_KEY!,`);
1838
+ }
1839
+ return `${providerId}: {
1840
+ ${configLines.join("\n ")}
1841
+ }`;
1842
+ }).join(",\n ");
1843
+ parts.push(`socialProviders: {
1844
+ ${providersObject}
1845
+ },`);
1846
+ }
1847
+ return parts.join("\n ");
1848
+ }
1849
+ function generateEnvTsServerSchema(providers) {
1850
+ if (providers.length === 0) {
1851
+ return "";
1852
+ }
1853
+ const schemas = providers.map((providerId) => {
1854
+ const provider = getProvider(providerId);
1855
+ if (!provider) {
1856
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
1857
+ }
1858
+ const lines = [];
1859
+ lines.push(`${provider.envPrefix}_CLIENT_ID: z.string(),`);
1860
+ lines.push(`${provider.envPrefix}_CLIENT_SECRET: z.string(),`);
1861
+ if (providerId === "figma") {
1862
+ lines.push(`FIGMA_CLIENT_KEY: z.string(),`);
1863
+ }
1864
+ return lines.join("\n ");
1865
+ }).join("\n ");
1866
+ return schemas;
1867
+ }
1868
+ function generateEnvTsRuntimeMapping(providers) {
1869
+ if (providers.length === 0) {
1870
+ return "";
1871
+ }
1872
+ const mappings = providers.map((providerId) => {
1873
+ const provider = getProvider(providerId);
1874
+ if (!provider) {
1875
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
1876
+ }
1877
+ const lines = [];
1878
+ lines.push(`${provider.envPrefix}_CLIENT_ID: process.env.${provider.envPrefix}_CLIENT_ID,`);
1879
+ lines.push(`${provider.envPrefix}_CLIENT_SECRET: process.env.${provider.envPrefix}_CLIENT_SECRET,`);
1880
+ if (providerId === "figma") {
1881
+ lines.push(`FIGMA_CLIENT_KEY: process.env.FIGMA_CLIENT_KEY,`);
1882
+ }
1883
+ return lines.join("\n ");
1884
+ }).join("\n ");
1885
+ return mappings;
1886
+ }
1887
+ function generateOAuthUIProvidersBlock(providers) {
1888
+ if (providers.length === 0) {
1889
+ return "";
1890
+ }
1891
+ const providerList = providers.map((id) => `"${id}"`).join(", ");
1892
+ return `social={{
1893
+ providers: [${providerList}]
1894
+ }}`;
1895
+ }
1896
+ function generateEnvVarsBlock(providers, framework) {
1897
+ if (providers.length === 0) {
1898
+ return "";
1899
+ }
1900
+ const envVars = providers.flatMap((providerId) => {
1901
+ const provider = getProvider(providerId);
1902
+ if (!provider) {
1903
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
1904
+ }
1905
+ if (provider.env && provider.env.length > 0) {
1906
+ return provider.env.map((envVar) => {
1907
+ let prefix = "";
1908
+ if (envVar.type === "client") {
1909
+ prefix = framework === "nextjs" ? "NEXT_PUBLIC_" : "VITE_";
1910
+ }
1911
+ return `# ${envVar.description}
1912
+ ${prefix}${envVar.name}=`;
1913
+ });
1914
+ } else {
1915
+ return [
1916
+ `${provider.clientIdVar}=`,
1917
+ `${provider.clientSecretVar}=`
1918
+ ];
1919
+ }
1920
+ });
1921
+ return envVars.join("\n");
1922
+ }
1923
+ function generateReadmeSection(providers) {
1924
+ if (providers.length === 0) {
1925
+ return "";
1926
+ }
1927
+ const sections = providers.map((providerId) => {
1928
+ const provider = getProvider(providerId);
1929
+ if (!provider) {
1930
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
1931
+ }
1932
+ if (!provider.readme) {
1933
+ throw new Error(
1934
+ `Provider ${providerId} missing readme metadata`
1935
+ );
1936
+ }
1937
+ return provider.readme.content;
1938
+ }).filter(Boolean);
1939
+ if (sections.length === 0) {
1940
+ return "";
1941
+ }
1942
+ return `# OAuth Provider Setup
2003
1943
 
2004
- **Important**: TikTok uses clientKey instead of clientId.
1944
+ ${sections.join("\n\n---\n\n")}`;
1945
+ }
1946
+ function assertAndReplace(filePath, content, oldBlock, newBlock, label) {
1947
+ if (!content.includes(oldBlock)) {
1948
+ throw new Error(
1949
+ `configureOrganizations: expected pattern "${label}" not found in ${filePath} \u2014 template drift?`
1950
+ );
1951
+ }
1952
+ return content.replace(oldBlock, newBlock);
1953
+ }
1954
+ function stripOrgFromGetAuthCall(filePath, content) {
1955
+ const importPattern = /import \{ TABLE_SLUG_ORGANIZATIONS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS \} from "~\/db\/constants";?/;
1956
+ const importMatch = content.match(importPattern);
1957
+ if (!importMatch) {
1958
+ throw new Error(
1959
+ `configureOrganizations: expected the TABLE_SLUG_ORGANIZATIONS db/constants import in ${filePath} \u2014 template drift?`
1960
+ );
1961
+ }
1962
+ let result = assertAndReplace(
1963
+ filePath,
1964
+ content,
1965
+ importMatch[0],
1966
+ importMatch[0].replace("TABLE_SLUG_ORGANIZATIONS, ", ""),
1967
+ "TABLE_SLUG_ORGANIZATIONS db/constants import"
1968
+ );
1969
+ const orgPropPattern = /\n[ \t]*orgCollectionSlug: TABLE_SLUG_ORGANIZATIONS,/;
1970
+ const orgPropMatch = result.match(orgPropPattern);
1971
+ if (!orgPropMatch) {
1972
+ throw new Error(
1973
+ `configureOrganizations: expected an "orgCollectionSlug: TABLE_SLUG_ORGANIZATIONS," line in ${filePath} \u2014 template drift?`
1974
+ );
1975
+ }
1976
+ result = assertAndReplace(filePath, result, orgPropMatch[0], "", "orgCollectionSlug prop");
1977
+ result = assertAndReplace(
1978
+ filePath,
1979
+ result,
1980
+ "resolveOrgs: true,",
1981
+ "resolveOrgs: false,",
1982
+ "resolveOrgs: true"
1983
+ );
1984
+ return result;
1985
+ }
1986
+ function stripOrgFromAuthApi(filePath, content) {
1987
+ let result = assertAndReplace(
1988
+ filePath,
1989
+ content,
1990
+ 'import { get } from "@vexcms/core/server";\nimport { v } from "convex/values";\n\nimport {\n type OrganizationID,\n type SessionID,\n TABLE_SLUG_ORGANIZATIONS,\n TABLE_SLUG_SESSIONS,\n TABLE_SLUG_USERS,\n type UserID,\n} from "~/db/constants";',
1991
+ 'import { get } from "@vexcms/core/server";\n\nimport { type SessionID, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS, type UserID } from "~/db/constants";',
1992
+ "auth/api.ts imports"
1993
+ );
1994
+ result = assertAndReplace(
1995
+ filePath,
1996
+ result,
1997
+ "export const getUserOrg = query({\n args: {\n noOrgs: v.optional(v.boolean()),\n },\n handler: async (ctx, args) => {\n const empty = { user: null, organization: undefined };\n const identity = await ctx.auth.getUserIdentity();\n if (!identity) {\n return empty;\n }\n const session = await get({\n ctx,\n id: identity.sessionId as SessionID,\n collection: TABLE_SLUG_SESSIONS,\n });\n if (!session) {\n return empty;\n }\n const user = await get({ ctx, id: identity.subject as UserID, collection: TABLE_SLUG_USERS });\n if (!user) {\n return empty;\n }\n if (!session.activeOrganizationId || args.noOrgs) {\n return { user, organization: undefined };\n }\n const organization = await get({\n ctx,\n id: session.activeOrganizationId as OrganizationID,\n collection: TABLE_SLUG_ORGANIZATIONS,\n });\n return { user, organization: organization ?? undefined };\n },\n});",
1998
+ "export const getUserOrg = query({\n args: {},\n handler: async (ctx) => {\n const empty = { user: null, organization: undefined };\n const identity = await ctx.auth.getUserIdentity();\n if (!identity) {\n return empty;\n }\n const session = await get({\n ctx,\n id: identity.sessionId as SessionID,\n collection: TABLE_SLUG_SESSIONS,\n });\n if (!session) {\n return empty;\n }\n const user = await get({ ctx, id: identity.subject as UserID, collection: TABLE_SLUG_USERS });\n if (!user) {\n return empty;\n }\n return { user, organization: undefined };\n },\n});",
1999
+ "auth/api.ts getUserOrg body"
2000
+ );
2001
+ return result;
2002
+ }
2003
+ function stripOrgFromAccess(filePath, content) {
2004
+ let result = assertAndReplace(
2005
+ filePath,
2006
+ content,
2007
+ 'import {\n TABLE_SLUG_ORGANIZATIONS,\n TABLE_SLUG_USERS,\n USER_ROLES,\n} from "~/db/constants"',
2008
+ 'import { TABLE_SLUG_USERS, USER_ROLES } from "~/db/constants"',
2009
+ "access.ts db/constants import"
2010
+ );
2011
+ result = assertAndReplace(
2012
+ filePath,
2013
+ result,
2014
+ "\n orgCollectionSlug: TABLE_SLUG_ORGANIZATIONS,",
2015
+ "",
2016
+ "access.ts orgCollectionSlug prop"
2017
+ );
2018
+ return result;
2019
+ }
2020
+ function stripOrgFromAuthContext(filePath, content) {
2021
+ let result = assertAndReplace(
2022
+ filePath,
2023
+ content,
2024
+ 'import { type OrganizationDoc, type UserDoc } from "~/db/constants";',
2025
+ 'import { type UserDoc } from "~/db/constants";',
2026
+ "AuthContext.tsx db/constants import"
2027
+ );
2028
+ result = assertAndReplace(
2029
+ filePath,
2030
+ result,
2031
+ "export interface CurrentAuth {\n organization?: OrganizationDoc;\n user: null | UserDoc; // null \u2192 unauthenticated \u2192 no roles \u2192 deny\n}",
2032
+ "export interface CurrentAuth {\n user: null | UserDoc; // null \u2192 unauthenticated \u2192 no roles \u2192 deny\n}",
2033
+ "AuthContext.tsx CurrentAuth interface"
2034
+ );
2035
+ result = assertAndReplace(
2036
+ filePath,
2037
+ result,
2038
+ " value: {\n organization?: OrganizationDoc;\n user: null | UserDoc; // null \u2192 unauthenticated \u2192 no roles \u2192 deny\n };",
2039
+ " value: {\n user: null | UserDoc; // null \u2192 unauthenticated \u2192 no roles \u2192 deny\n };",
2040
+ "AuthContext.tsx AuthProvider value prop"
2041
+ );
2042
+ result = assertAndReplace(
2043
+ filePath,
2044
+ result,
2045
+ "/** @returns the current caller `{ user, organization }` from the server layout. */",
2046
+ "/** @returns the current caller `{ user }` from the server layout. */",
2047
+ "AuthContext.tsx useAuth JSDoc"
2048
+ );
2049
+ return result;
2050
+ }
2051
+ function stripOrgFromHasPermission(filePath, content) {
2052
+ return assertAndReplace(
2053
+ filePath,
2054
+ content,
2055
+ " const { user, organization } = useAuth();\n return hasPermissionCore({\n access,\n user,\n organization,\n ...props,\n });",
2056
+ " const { user } = useAuth();\n return hasPermissionCore({\n access,\n user,\n ...props,\n });",
2057
+ "hasPermission.ts useAuth destructure + hasPermissionCore call"
2058
+ );
2059
+ }
2060
+ function stripOrgFromDbConstants(filePath, content) {
2061
+ return assertAndReplace(
2062
+ filePath,
2063
+ content,
2064
+ '\nexport const TABLE_SLUG_ORGANIZATIONS = "organization" as const\nexport type OrganizationDoc = Doc<typeof TABLE_SLUG_ORGANIZATIONS>\nexport type OrganizationID = Id<typeof TABLE_SLUG_ORGANIZATIONS>\n',
2065
+ "",
2066
+ "db/constants/index.ts organization table block"
2067
+ );
2068
+ }
2005
2069
 
2006
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
2070
+ // src/installers/base.ts
2071
+ var MARKETING_DEPENDENCIES = {
2072
+ /** Render-time syntax highlighting for the CodeShowcase and Split blocks. */
2073
+ shiki: "^4.4.3"
2074
+ };
2075
+ var VexFrameworkInstaller = class {
2076
+ /**
2077
+ * Binds this installer to the destination directory and package name used
2078
+ * throughout scaffolding (template copy target, package.json, README, etc).
2079
+ *
2080
+ * @param targetPath - Absolute path of the directory the project is created in.
2081
+ * @param projectName - npm package name recorded for subclasses; not read directly by the base class.
2082
+ */
2083
+ constructor(targetPath, projectName) {
2084
+ this.targetPath = targetPath;
2085
+ this.projectName = projectName;
2086
+ }
2087
+ targetPath;
2088
+ projectName;
2089
+ /**
2090
+ * Copy base template files to target directory
2091
+ */
2092
+ async copyBaseFiles() {
2093
+ await copyTemplate(this.frameworkName, this.targetPath);
2094
+ }
2095
+ /**
2096
+ * Apply a template overlay on top of the base template.
2097
+ * Merges overlay files onto the already-copied base — existing files
2098
+ * not in the overlay are left untouched.
2099
+ *
2100
+ * @param overlay - Name of the overlay template directory under `templates/` (e.g. `"marketing-site"`).
2101
+ */
2102
+ async applyTemplateOverlay(overlay) {
2103
+ const { dirname: dirname2 } = await import("path");
2104
+ const { fileURLToPath: fileURLToPath2 } = await import("url");
2105
+ const __filename = fileURLToPath2(import.meta.url);
2106
+ const __dirname = dirname2(__filename);
2107
+ let overlayDir = path2.join(__dirname, "../../templates", overlay);
2108
+ if (!await fs5.pathExists(overlayDir)) {
2109
+ overlayDir = path2.join(__dirname, "../templates", overlay);
2110
+ }
2111
+ await overlayTemplate({ overlayDir, targetDir: this.targetPath });
2112
+ }
2113
+ /**
2114
+ * Apply the parts of the marketing overlay that copying files cannot express.
2115
+ *
2116
+ * `overlayTemplate` only adds and overwrites — it has no way to remove a
2117
+ * base file, and it does not touch `package.json`. Two things therefore
2118
+ * have to happen here:
2119
+ *
2120
+ * 1. **Delete base's `(frontend)/page.tsx`.** It and the overlay's
2121
+ * `(frontend)/(site)/page.tsx` both resolve to `/`. Next picks base's,
2122
+ * so the marketing home page is unreachable and every scaffold shows the
2123
+ * bare bootstrap `WelcomePage` instead of the seeded site. The `(site)`
2124
+ * route group owns `/` in a marketing scaffold.
2125
+ * 2. **Add `shiki`.** The CodeShowcase and Split blocks highlight at render
2126
+ * time in a server component. It is a marketing-only dependency, so it
2127
+ * is added here rather than carried in base's `package.json` where a
2128
+ * `--bare` scaffold would install it for nothing.
2129
+ */
2130
+ async finalizeMarketingOverlay() {
2131
+ const homeRoute = path2.join(this.targetPath, "src/app/(frontend)/page.tsx");
2132
+ await fs5.remove(homeRoute);
2133
+ const pkgPath = path2.join(this.targetPath, "package.json");
2134
+ const pkg = await fs5.readJson(pkgPath);
2135
+ pkg.dependencies = { ...pkg.dependencies, shiki: MARKETING_DEPENDENCIES.shiki };
2136
+ pkg.dependencies = Object.fromEntries(
2137
+ Object.entries(pkg.dependencies).sort(([a], [b]) => a.localeCompare(b))
2138
+ );
2139
+ await fs5.writeJson(pkgPath, pkg, { spaces: 2 });
2140
+ }
2141
+ /**
2142
+ * Update the package.json name field, the vex:update script (to match the
2143
+ * detected package manager), and README.md's `{{PROJECT_NAME}}` heading.
2144
+ *
2145
+ * @param name - npm package name to write into `package.json`'s `name` field
2146
+ * and to substitute for `{{PROJECT_NAME}}` in `README.md`.
2147
+ */
2148
+ async updatePackageName(name) {
2149
+ const pkgPath = path2.join(this.targetPath, "package.json");
2150
+ const pkg = await fs5.readJson(pkgPath);
2151
+ pkg.name = name;
2152
+ const pm = this.detectPackageManager();
2153
+ if (pkg.scripts?.["vex:update"] && pm !== "pnpm") {
2154
+ const addCmd = pm === "yarn" ? "yarn add" : pm === "bun" ? "bun add" : "npm install";
2155
+ pkg.scripts["vex:update"] = pkg.scripts["vex:update"].replace(/^pnpm add/, addCmd);
2156
+ }
2157
+ await fs5.writeJson(pkgPath, pkg, { spaces: 2 });
2158
+ const readmePath = path2.join(this.targetPath, "README.md");
2159
+ if (await fs5.pathExists(readmePath)) {
2160
+ const readme = await fs5.readFile(readmePath, "utf-8");
2161
+ await fs5.writeFile(readmePath, readme.replace("{{PROJECT_NAME}}", name));
2162
+ }
2163
+ }
2164
+ /**
2165
+ * Configure the dev server port.
2166
+ * Updates package.json dev script and creates .env.local with NEXT_PUBLIC_SITE_URL.
2167
+ *
2168
+ * @param port - Dev server port number to write into the `dev` script and `.env.local`.
2169
+ */
2170
+ async configurePort(port) {
2171
+ const pkgPath = path2.join(this.targetPath, "package.json");
2172
+ const pkg = await fs5.readJson(pkgPath);
2173
+ if (pkg.scripts?.dev) {
2174
+ if (pkg.scripts.dev.includes("--port")) {
2175
+ pkg.scripts.dev = pkg.scripts.dev.replace(/--port[= ]\d+/, `--port=${port}`);
2176
+ } else {
2177
+ pkg.scripts.dev = `${pkg.scripts.dev} --port=${port}`;
2178
+ }
2179
+ }
2180
+ await fs5.writeJson(pkgPath, pkg, { spaces: 2 });
2181
+ const siteUrl = `http://localhost:${port}`;
2182
+ const envLocalPath = path2.join(this.targetPath, ".env.local");
2183
+ const envContent = [
2184
+ `NEXT_PUBLIC_SITE_URL=${siteUrl}`,
2185
+ `SITE_URL=${siteUrl}`,
2186
+ `BETTER_AUTH_SECRET=${this.generateAuthSecret()}`,
2187
+ `NEXT_PUBLIC_CONVEX_URL=https://placeholder.convex.cloud`,
2188
+ `NEXT_PUBLIC_CONVEX_SITE_URL=https://placeholder.convex.site`,
2189
+ `CONVEX_DEPLOYMENT=`,
2190
+ ""
2191
+ ].join("\n");
2192
+ await fs5.writeFile(envLocalPath, envContent);
2193
+ }
2194
+ /**
2195
+ * Rewrites the scaffolded `package.json` for life inside a host pnpm
2196
+ * workspace (`--monorepo`). `@vexcms/*` dependencies become
2197
+ * `workspace:*`; any other dependency the host's `pnpm-workspace.yaml`
2198
+ * catalog also pins becomes `catalog:`; everything else keeps its
2199
+ * literal, catalog-resolved version. No-op when `options.monorepo` is
2200
+ * false or `options.workspaceRoot` is unset.
2201
+ *
2202
+ * @param options - Resolved project options; only `monorepo`/`workspaceRoot` are consulted.
2203
+ */
2204
+ async applyMonorepoRewrite(options) {
2205
+ if (!options.monorepo || !options.workspaceRoot) return;
2206
+ const pkgPath = path2.join(this.targetPath, "package.json");
2207
+ const manifest = await fs5.readJson(pkgPath);
2208
+ const catalog = await readWorkspaceCatalog({ workspaceRoot: options.workspaceRoot });
2209
+ const rewritten = rewriteManifestForMonorepo({ manifest, catalog });
2210
+ await fs5.writeJson(pkgPath, rewritten, { spaces: 2 });
2211
+ }
2212
+ /**
2213
+ * Detect the package manager used to invoke the CLI
2214
+ *
2215
+ * @returns The package manager inferred from `npm_config_user_agent`, defaulting to `"npm"` when it can't be determined.
2216
+ */
2217
+ detectPackageManager() {
2218
+ const userAgent = process.env.npm_config_user_agent;
2219
+ if (userAgent) {
2220
+ if (userAgent.includes("pnpm")) return "pnpm";
2221
+ if (userAgent.includes("yarn")) return "yarn";
2222
+ if (userAgent.includes("bun")) return "bun";
2223
+ }
2224
+ return "npm";
2225
+ }
2226
+ /**
2227
+ * Install project dependencies using detected package manager
2228
+ */
2229
+ async installDependencies() {
2230
+ const packageManager = this.detectPackageManager();
2231
+ const spinner = ora(`Installing dependencies with ${packageManager}...`).start();
2232
+ try {
2233
+ await execa(packageManager, ["install"], {
2234
+ cwd: this.targetPath,
2235
+ stdio: "pipe"
2236
+ });
2237
+ spinner.succeed("Dependencies installed successfully");
2238
+ } catch (error) {
2239
+ spinner.fail("Failed to install dependencies");
2240
+ throw new Error(
2241
+ `Dependency installation failed: ${error instanceof Error ? error.message : "Unknown error"}`
2242
+ );
2007
2243
  }
2008
- },
2009
- vercel: {
2010
- id: "vercel",
2011
- name: "Vercel",
2012
- envPrefix: "VERCEL",
2013
- clientIdVar: "VERCEL_CLIENT_ID",
2014
- clientSecretVar: "VERCEL_CLIENT_SECRET",
2015
- popular: false,
2016
- betterAuthConfig: {
2017
- import: "",
2018
- clientSideProvider: '"vercel"',
2019
- socialProvider: `vercel({
2020
- clientId: process.env.VERCEL_CLIENT_ID!,
2021
- clientSecret: process.env.VERCEL_CLIENT_SECRET!,
2022
- })`,
2023
- scopes: []
2024
- },
2025
- env: [
2026
- {
2027
- name: "VERCEL_CLIENT_ID",
2028
- type: "server",
2029
- description: "Vercel OAuth Client ID"
2030
- },
2031
- {
2032
- name: "VERCEL_CLIENT_SECRET",
2033
- type: "server",
2034
- description: "Vercel OAuth Client Secret"
2244
+ }
2245
+ /**
2246
+ * Format code using project's formatter
2247
+ */
2248
+ async formatCode() {
2249
+ const packageManager = this.detectPackageManager();
2250
+ const spinner = ora("Formatting code...").start();
2251
+ try {
2252
+ const result = await execa(packageManager, ["run", "format"], {
2253
+ cwd: this.targetPath,
2254
+ stdio: "pipe",
2255
+ reject: false
2256
+ });
2257
+ if (result.exitCode === 0) {
2258
+ spinner.succeed("Code formatted successfully");
2259
+ } else {
2260
+ spinner.warn("Formatting completed with warnings");
2035
2261
  }
2036
- ],
2037
- docs: {
2038
- provider: "https://vercel.com/account/integrations",
2039
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
2040
- },
2041
- requiresExtraConfig: false,
2042
- extraConfigNotes: "Uses PKCE for enhanced security.",
2043
- readme: {
2044
- title: "Vercel OAuth Setup",
2045
- content: `## Vercel OAuth Setup
2046
-
2047
- 1. Create an integration at https://vercel.com/account/integrations
2048
- 2. Configure OAuth settings
2049
- 3. Add redirect URL: \`http://localhost:3000/api/auth/callback/vercel\` (update for production)
2050
- 4. Copy the Client ID and Client Secret to your \`.env\` file
2051
-
2052
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
2262
+ } catch (error) {
2263
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
2264
+ spinner.warn(`Failed to format code: ${errorMessage}`);
2053
2265
  }
2054
- },
2055
- vk: {
2056
- id: "vk",
2057
- name: "VK",
2058
- envPrefix: "VK",
2059
- clientIdVar: "VK_CLIENT_ID",
2060
- clientSecretVar: "VK_CLIENT_SECRET",
2061
- popular: false,
2062
- betterAuthConfig: {
2063
- import: "",
2064
- clientSideProvider: '"vk"',
2065
- socialProvider: `vk({
2066
- clientId: process.env.VK_CLIENT_ID!,
2067
- clientSecret: process.env.VK_CLIENT_SECRET!,
2068
- })`,
2069
- scopes: []
2070
- },
2071
- env: [
2072
- {
2073
- name: "VK_CLIENT_ID",
2074
- type: "server",
2075
- description: "VK Application ID"
2076
- },
2077
- {
2078
- name: "VK_CLIENT_SECRET",
2079
- type: "server",
2080
- description: "VK Secure Key"
2266
+ }
2267
+ /**
2268
+ * Lint and fix code using project's ESLint configuration
2269
+ */
2270
+ async lintCode() {
2271
+ const packageManager = this.detectPackageManager();
2272
+ const spinner = ora("Linting and fixing code...").start();
2273
+ try {
2274
+ const args2 = packageManager === "pnpm" ? ["lint", "--fix"] : ["run", "lint", "--", "--fix"];
2275
+ const result = await execa(packageManager, args2, {
2276
+ cwd: this.targetPath,
2277
+ stdio: "pipe",
2278
+ reject: false
2279
+ });
2280
+ if (result.exitCode === 0) {
2281
+ spinner.succeed("Code linted and fixed successfully");
2282
+ } else {
2283
+ spinner.warn("Linting completed with warnings");
2081
2284
  }
2082
- ],
2083
- docs: {
2084
- provider: "https://vk.com/apps?act=manage",
2085
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
2086
- },
2087
- requiresExtraConfig: false,
2088
- extraConfigNotes: "",
2089
- readme: {
2090
- title: "VK OAuth Setup",
2091
- content: `## VK OAuth Setup
2092
-
2093
- 1. Create an app at https://vk.com/apps?act=manage
2094
- 2. Configure OAuth settings in the app
2095
- 3. Add authorized redirect URI: \`http://localhost:3000/api/auth/callback/vk\` (update for production)
2096
- 4. Copy the Application ID and Secure Key to your \`.env\` file
2097
-
2098
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
2285
+ } catch (error) {
2286
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
2287
+ spinner.warn(`Failed to lint code: ${errorMessage}`);
2099
2288
  }
2100
- },
2101
- zoom: {
2102
- id: "zoom",
2103
- name: "Zoom",
2104
- envPrefix: "ZOOM",
2105
- clientIdVar: "ZOOM_CLIENT_ID",
2106
- clientSecretVar: "ZOOM_CLIENT_SECRET",
2107
- popular: false,
2108
- betterAuthConfig: {
2109
- import: "",
2110
- clientSideProvider: '"zoom"',
2111
- socialProvider: `zoom({
2112
- clientId: process.env.ZOOM_CLIENT_ID!,
2113
- clientSecret: process.env.ZOOM_CLIENT_SECRET!,
2114
- })`,
2115
- scopes: ["user:read:user"]
2116
- },
2117
- env: [
2118
- {
2119
- name: "ZOOM_CLIENT_ID",
2120
- type: "server",
2121
- description: "Zoom OAuth Client ID"
2122
- },
2123
- {
2124
- name: "ZOOM_CLIENT_SECRET",
2125
- type: "server",
2126
- description: "Zoom OAuth Client Secret"
2289
+ }
2290
+ /**
2291
+ * Initialize Git repository in target directory
2292
+ */
2293
+ async initGitRepo() {
2294
+ const spinner = ora("Initializing Git repository...").start();
2295
+ try {
2296
+ try {
2297
+ await execa("git", ["--version"], { stdio: "pipe" });
2298
+ } catch {
2299
+ spinner.fail("Git is not installed");
2300
+ throw new Error("Git is not installed. Please install Git to initialize a repository.");
2127
2301
  }
2128
- ],
2129
- docs: {
2130
- provider: "https://marketplace.zoom.us/",
2131
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
2132
- },
2133
- requiresExtraConfig: true,
2134
- extraConfigNotes: "You MUST include the user:read:user scope for Zoom.",
2135
- readme: {
2136
- title: "Zoom OAuth Setup",
2137
- content: `## Zoom OAuth Setup
2138
-
2139
- 1. Create an app at https://marketplace.zoom.us/
2140
- 2. Choose OAuth as the app type
2141
- 3. Add redirect URL: \`http://localhost:3000/api/auth/callback/zoom\` (update for production)
2142
- 4. Copy the Client ID and Client Secret to your \`.env\` file
2143
- 5. **Important**: Make sure to include the \`user:read:user\` scope
2144
-
2145
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
2302
+ await execa("git", ["init"], { cwd: this.targetPath, stdio: "pipe" });
2303
+ await execa("git", ["add", "."], { cwd: this.targetPath, stdio: "pipe" });
2304
+ await execa(
2305
+ "git",
2306
+ ["commit", "-m", "Initial commit from create-vexcms"],
2307
+ { cwd: this.targetPath, stdio: "pipe" }
2308
+ );
2309
+ spinner.succeed("Git repository initialized");
2310
+ } catch (error) {
2311
+ spinner.fail("Failed to initialize Git repository");
2312
+ throw new Error(
2313
+ `Git initialization failed: ${error instanceof Error ? error.message : "Unknown error"}`
2314
+ );
2146
2315
  }
2147
2316
  }
2148
- };
2149
- function getProvider(id) {
2150
- return OAUTH_PROVIDERS[id];
2151
- }
2152
- function getPopularProviders() {
2153
- return Object.values(OAUTH_PROVIDERS).filter(
2154
- (provider) => provider.popular === true
2155
- );
2156
- }
2157
- function getAdditionalProviders() {
2158
- return Object.values(OAUTH_PROVIDERS).filter(
2159
- (provider) => provider.popular !== true
2160
- );
2161
- }
2162
-
2163
- // src/installers/string-utils.ts
2164
- function detectIndentation(line) {
2165
- const match = line.match(/^(\s*)/);
2166
- return match ? match[1] : "";
2167
- }
2168
- async function replacePlaceholder(filePath, placeholder, content, options) {
2169
- const fileContent = await fs5.readFile(filePath, "utf-8");
2170
- if (!fileContent.includes(placeholder)) {
2171
- if (options?.graceful) {
2172
- console.warn(
2173
- `Warning: Placeholder "${placeholder}" not found in file: ${filePath}. Skipping replacement.`
2317
+ /**
2318
+ * Generate a secure random secret for Better Auth
2319
+ *
2320
+ * @returns A 64-character hex string (32 random bytes) suitable for `BETTER_AUTH_SECRET`.
2321
+ */
2322
+ generateAuthSecret() {
2323
+ return crypto.randomBytes(32).toString("hex");
2324
+ }
2325
+ /**
2326
+ * Write the generated auth secret to .env.example
2327
+ */
2328
+ async writeAuthSecret() {
2329
+ const envPath = path2.join(this.targetPath, ".env.example");
2330
+ const secret = this.generateAuthSecret();
2331
+ if (await fs5.pathExists(envPath)) {
2332
+ let content = await fs5.readFile(envPath, "utf-8");
2333
+ content = content.replace(
2334
+ /BETTER_AUTH_SECRET=.*/,
2335
+ `BETTER_AUTH_SECRET=${secret}`
2174
2336
  );
2175
- return;
2337
+ await fs5.writeFile(envPath, content);
2338
+ }
2339
+ }
2340
+ /**
2341
+ * Configure the Better Auth organizations plugin.
2342
+ * When enabled, replaces placeholders with the organization import, plugin
2343
+ * instance, and schema descriptor,
2344
+ * and leaves every org-aware file (`createGetAuth` call sites, `access.ts`,
2345
+ * `AuthContext.tsx`, `hasPermission.ts`, `convex/auth/api.ts`,
2346
+ * `db/constants/index.ts`) untouched.
2347
+ * When disabled, removes the placeholder lines AND strips organization
2348
+ * wiring from those same files down to the org-free shape — the
2349
+ * templates ship org-enabled by default (so `--orgs` needs no surgery),
2350
+ * so the default (orgs-declined) path has to actively cut it.
2351
+ *
2352
+ * @param enabled - Whether the organizations (multi-tenant) plugin should stay wired.
2353
+ */
2354
+ async configureOrganizations(enabled) {
2355
+ const pluginsPath = path2.join(this.targetPath, "convex/auth/plugins/index.ts");
2356
+ if (await fs5.pathExists(pluginsPath)) {
2357
+ let content = await fs5.readFile(pluginsPath, "utf-8");
2358
+ if (enabled) {
2359
+ content = content.replace(
2360
+ "// {{ORGANIZATIONS_IMPORT}}",
2361
+ 'import { organization } from "better-auth/plugins"'
2362
+ );
2363
+ content = content.replace(
2364
+ " // {{ORGANIZATIONS_PLUGIN}}",
2365
+ " organization(),"
2366
+ );
2367
+ } else {
2368
+ content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_IMPORT\}\}\n?/, "");
2369
+ content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_PLUGIN\}\}\n?/, "");
2370
+ }
2371
+ await fs5.writeFile(pluginsPath, content);
2372
+ }
2373
+ const schemaPath = path2.join(this.targetPath, "convex/auth/schema.ts");
2374
+ if (await fs5.pathExists(schemaPath)) {
2375
+ let content = await fs5.readFile(schemaPath, "utf-8");
2376
+ content = enabled ? content.replace(" // {{ORGANIZATIONS_SCHEMA}}", " organization: true,") : content.replace(/.*\/\/ \{\{ORGANIZATIONS_SCHEMA\}\}\n?/, "");
2377
+ await fs5.writeFile(schemaPath, content);
2378
+ }
2379
+ if (!enabled) {
2380
+ await this.stripOrganizationWiring();
2176
2381
  }
2177
- throw new Error(
2178
- `Placeholder "${placeholder}" not found in file: ${filePath}`
2179
- );
2180
- }
2181
- if (options?.inline) {
2182
- const updatedContent = fileContent.replace(placeholder, content);
2183
- await fs5.writeFile(filePath, updatedContent, "utf-8");
2184
- return;
2185
2382
  }
2186
- const lines = fileContent.split("\n");
2187
- const updatedLines = [];
2188
- for (const line of lines) {
2189
- if (line.includes(placeholder)) {
2190
- if (content === "" || content.startsWith("__REMOVE_")) {
2191
- continue;
2383
+ /**
2384
+ * Rewrites every scaffolded file that references organizations down to
2385
+ * its org-free shape. Called by `configureOrganizations(false)` — the
2386
+ * `--orgs`-declined default. Templates ship org-ENABLED (so `--orgs`
2387
+ * requires no surgery); this is what makes the default scaffold buildable
2388
+ * once the generated registry drops org tables on the first `vex dev`
2389
+ * regen. Every transform throws loudly when its expected pattern is
2390
+ * missing (template drift), rather than silently no-opping.
2391
+ *
2392
+ * `src/vexcms/api.ts` only exists in full (marketing-site overlay)
2393
+ * scaffolds — skipped, not required, for `--bare`.
2394
+ */
2395
+ async stripOrganizationWiring() {
2396
+ const requiredFiles = [
2397
+ { rel: "convex/vex.ts", transform: stripOrgFromGetAuthCall },
2398
+ { rel: "convex/vex/globals.ts", transform: stripOrgFromGetAuthCall },
2399
+ { rel: "convex/vex/media.ts", transform: stripOrgFromGetAuthCall },
2400
+ { rel: "convex/auth/api.ts", transform: stripOrgFromAuthApi },
2401
+ { rel: "src/auth/access.ts", transform: stripOrgFromAccess },
2402
+ { rel: "src/context/AuthContext.tsx", transform: stripOrgFromAuthContext },
2403
+ { rel: "src/auth/hasPermission.ts", transform: stripOrgFromHasPermission },
2404
+ { rel: "src/db/constants/index.ts", transform: stripOrgFromDbConstants }
2405
+ ];
2406
+ for (const file of requiredFiles) {
2407
+ const filePath = path2.join(this.targetPath, file.rel);
2408
+ if (!await fs5.pathExists(filePath)) {
2409
+ throw new Error(
2410
+ `configureOrganizations: required file "${file.rel}" is missing from the scaffold \u2014 template drift?`
2411
+ );
2192
2412
  }
2193
- const indentation = detectIndentation(line);
2194
- const indentedContent = content.split("\n").map((contentLine, index) => {
2195
- if (index === 0) {
2196
- return indentation + contentLine;
2197
- }
2198
- return contentLine ? indentation + contentLine : "";
2199
- }).join("\n");
2200
- updatedLines.push(indentedContent);
2201
- } else {
2202
- updatedLines.push(line);
2413
+ const content = await fs5.readFile(filePath, "utf-8");
2414
+ await fs5.writeFile(filePath, file.transform(filePath, content));
2415
+ }
2416
+ const apiPath = path2.join(this.targetPath, "src/vexcms/api.ts");
2417
+ if (await fs5.pathExists(apiPath)) {
2418
+ const content = await fs5.readFile(apiPath, "utf-8");
2419
+ await fs5.writeFile(apiPath, stripOrgFromGetAuthCall(apiPath, content));
2203
2420
  }
2204
2421
  }
2205
- await fs5.writeFile(filePath, updatedLines.join("\n"), "utf-8");
2206
- }
2207
- function generateCredentialsValue(enabled) {
2208
- return `credentials={${enabled}}`;
2209
- }
2210
- function generateAuthProvidersBlock(oauthProviders, emailPasswordEnabled) {
2211
- const parts = [];
2212
- parts.push(`emailAndPassword: {
2213
- enabled: ${emailPasswordEnabled}
2214
- },`);
2215
- if (oauthProviders.length > 0) {
2216
- const providersObject = oauthProviders.map((providerId) => {
2217
- const provider = getProvider(providerId);
2218
- if (!provider) {
2219
- throw new Error(`Unknown OAuth provider: ${providerId}`);
2220
- }
2221
- const configLines = [
2222
- `clientId: process.env.${provider.envPrefix}_CLIENT_ID!,`,
2223
- `clientSecret: process.env.${provider.envPrefix}_CLIENT_SECRET!,`
2224
- ];
2225
- if (providerId === "figma") {
2226
- configLines.push(`clientKey: process.env.FIGMA_CLIENT_KEY!,`);
2422
+ /**
2423
+ * Main orchestration method for project initialization
2424
+ *
2425
+ * @param options - Resolved project options from CLI flags and prompts, driving every scaffolding step (template copy, overlay, port, auth, README, git, install).
2426
+ */
2427
+ async initProject(options) {
2428
+ const copySpinner = ora("Copying template files...").start();
2429
+ try {
2430
+ await this.copyBaseFiles();
2431
+ copySpinner.succeed("Template files copied");
2432
+ } catch (error) {
2433
+ copySpinner.fail("Failed to copy template files");
2434
+ throw error;
2435
+ }
2436
+ if (!options.bare) {
2437
+ const overlaySpinner = ora("Applying marketing site template...").start();
2438
+ try {
2439
+ await this.applyTemplateOverlay("marketing-site");
2440
+ await this.finalizeMarketingOverlay();
2441
+ overlaySpinner.succeed("Marketing site template applied");
2442
+ } catch (error) {
2443
+ overlaySpinner.fail("Failed to apply template overlay");
2444
+ throw error;
2227
2445
  }
2228
- return `${providerId}: {
2229
- ${configLines.join("\n ")}
2230
- }`;
2231
- }).join(",\n ");
2232
- parts.push(`socialProviders: {
2233
- ${providersObject}
2234
- },`);
2235
- }
2236
- return parts.join("\n ");
2237
- }
2238
- function generateEnvTsServerSchema(providers) {
2239
- if (providers.length === 0) {
2240
- return "";
2241
- }
2242
- const schemas = providers.map((providerId) => {
2243
- const provider = getProvider(providerId);
2244
- if (!provider) {
2245
- throw new Error(`Unknown OAuth provider: ${providerId}`);
2246
2446
  }
2247
- const lines = [];
2248
- lines.push(`${provider.envPrefix}_CLIENT_ID: z.string(),`);
2249
- lines.push(`${provider.envPrefix}_CLIENT_SECRET: z.string(),`);
2250
- if (providerId === "figma") {
2251
- lines.push(`FIGMA_CLIENT_KEY: z.string(),`);
2447
+ const nameSpinner = ora("Configuring project...").start();
2448
+ try {
2449
+ await this.updatePackageName(options.projectName);
2450
+ await this.configurePort(options.port);
2451
+ nameSpinner.succeed("Project configured");
2452
+ } catch (error) {
2453
+ nameSpinner.fail("Failed to configure project");
2454
+ throw error;
2252
2455
  }
2253
- return lines.join("\n ");
2254
- }).join("\n ");
2255
- return schemas;
2256
- }
2257
- function generateEnvTsRuntimeMapping(providers) {
2258
- if (providers.length === 0) {
2259
- return "";
2260
- }
2261
- const mappings = providers.map((providerId) => {
2262
- const provider = getProvider(providerId);
2263
- if (!provider) {
2264
- throw new Error(`Unknown OAuth provider: ${providerId}`);
2456
+ const authSpinner = ora("Configuring authentication...").start();
2457
+ try {
2458
+ await this.updateOAuthConfig(options.oauthProviders, options.emailPasswordAuth);
2459
+ authSpinner.succeed(
2460
+ options.emailPasswordAuth || options.oauthProviders.length > 0 ? "Authentication configuration updated" : "Authentication placeholders cleaned up"
2461
+ );
2462
+ } catch (error) {
2463
+ authSpinner.fail("Failed to configure authentication");
2464
+ throw error;
2265
2465
  }
2266
- const lines = [];
2267
- lines.push(`${provider.envPrefix}_CLIENT_ID: process.env.${provider.envPrefix}_CLIENT_ID,`);
2268
- lines.push(`${provider.envPrefix}_CLIENT_SECRET: process.env.${provider.envPrefix}_CLIENT_SECRET,`);
2269
- if (providerId === "figma") {
2270
- lines.push(`FIGMA_CLIENT_KEY: process.env.FIGMA_CLIENT_KEY,`);
2466
+ const oauthUISpinner = ora("Configuring OAuth UI...").start();
2467
+ try {
2468
+ await this.updateOAuthUIConfig(options.oauthProviders, options.emailPasswordAuth);
2469
+ oauthUISpinner.succeed(
2470
+ options.oauthProviders.length > 0 ? "OAuth UI configuration updated" : "OAuth UI placeholders cleaned up"
2471
+ );
2472
+ } catch (error) {
2473
+ oauthUISpinner.fail("Failed to configure OAuth UI");
2474
+ throw error;
2271
2475
  }
2272
- return lines.join("\n ");
2273
- }).join("\n ");
2274
- return mappings;
2275
- }
2276
- function generateOAuthUIProvidersBlock(providers) {
2277
- if (providers.length === 0) {
2278
- return "";
2279
- }
2280
- const providerList = providers.map((id) => `"${id}"`).join(", ");
2281
- return `social={{
2282
- providers: [${providerList}]
2283
- }}`;
2284
- }
2285
- function generateEnvVarsBlock(providers, framework) {
2286
- if (providers.length === 0) {
2287
- return "";
2288
- }
2289
- const envVars = providers.flatMap((providerId) => {
2290
- const provider = getProvider(providerId);
2291
- if (!provider) {
2292
- throw new Error(`Unknown OAuth provider: ${providerId}`);
2476
+ const envSpinner = ora("Updating .env.example...").start();
2477
+ try {
2478
+ await this.updateEnvExample(options.oauthProviders);
2479
+ envSpinner.succeed(
2480
+ options.oauthProviders.length > 0 ? ".env.example updated" : ".env.example placeholders cleaned up"
2481
+ );
2482
+ } catch (error) {
2483
+ envSpinner.fail("Failed to update .env.example");
2484
+ throw error;
2293
2485
  }
2294
- if (provider.env && provider.env.length > 0) {
2295
- return provider.env.map((envVar) => {
2296
- let prefix = "";
2297
- if (envVar.type === "client") {
2298
- prefix = framework === "nextjs" ? "NEXT_PUBLIC_" : "VITE_";
2299
- }
2300
- return `# ${envVar.description}
2301
- ${prefix}${envVar.name}=`;
2302
- });
2303
- } else {
2304
- return [
2305
- `${provider.clientIdVar}=`,
2306
- `${provider.clientSecretVar}=`
2307
- ];
2486
+ const envTsSpinner = ora("Updating typed env configuration...").start();
2487
+ try {
2488
+ await this.updateEnvTs(options.oauthProviders);
2489
+ envTsSpinner.succeed(
2490
+ options.oauthProviders.length > 0 ? "Typed env configuration updated" : "Typed env placeholders cleaned up"
2491
+ );
2492
+ } catch (error) {
2493
+ envTsSpinner.fail("Failed to update typed env configuration");
2494
+ throw error;
2308
2495
  }
2309
- });
2310
- return envVars.join("\n");
2311
- }
2312
- function generateReadmeSection(providers) {
2313
- if (providers.length === 0) {
2314
- return "";
2315
- }
2316
- const sections = providers.map((providerId) => {
2317
- const provider = getProvider(providerId);
2318
- if (!provider) {
2319
- throw new Error(`Unknown OAuth provider: ${providerId}`);
2496
+ const readmeSpinner = ora("Updating README...").start();
2497
+ try {
2498
+ await this.updateReadme(options.oauthProviders);
2499
+ readmeSpinner.succeed(
2500
+ options.oauthProviders.length > 0 ? "README updated" : "README placeholders cleaned up"
2501
+ );
2502
+ } catch (error) {
2503
+ readmeSpinner.fail("Failed to update README");
2504
+ throw error;
2320
2505
  }
2321
- if (!provider.readme) {
2322
- throw new Error(
2323
- `Provider ${providerId} missing readme metadata`
2506
+ const orgsSpinner = ora("Configuring organizations...").start();
2507
+ try {
2508
+ await this.configureOrganizations(options.orgs);
2509
+ orgsSpinner.succeed(
2510
+ options.orgs ? "Organizations plugin enabled" : "Organizations placeholders cleaned up"
2324
2511
  );
2512
+ } catch (error) {
2513
+ orgsSpinner.fail("Failed to configure organizations");
2514
+ throw error;
2515
+ }
2516
+ const secretSpinner = ora("Generating auth secret...").start();
2517
+ try {
2518
+ await this.writeAuthSecret();
2519
+ secretSpinner.succeed("Auth secret generated");
2520
+ } catch (error) {
2521
+ secretSpinner.fail("Failed to generate auth secret");
2522
+ throw error;
2523
+ }
2524
+ if (options.monorepo) {
2525
+ const monorepoSpinner = ora("Rewriting dependencies for the host workspace...").start();
2526
+ try {
2527
+ await this.applyMonorepoRewrite(options);
2528
+ monorepoSpinner.succeed("Dependencies rewritten for the host workspace");
2529
+ } catch (error) {
2530
+ monorepoSpinner.fail("Failed to rewrite dependencies for the host workspace");
2531
+ throw error;
2532
+ }
2533
+ }
2534
+ if (options.initGit && !options.monorepo) {
2535
+ await this.initGitRepo();
2536
+ }
2537
+ if (options.installDependencies && !options.monorepo) {
2538
+ await this.installDependencies();
2539
+ await this.lintCode();
2540
+ await this.formatCode();
2325
2541
  }
2326
- return provider.readme.content;
2327
- }).filter(Boolean);
2328
- if (sections.length === 0) {
2329
- return "";
2330
2542
  }
2331
- return `# OAuth Provider Setup
2332
-
2333
- ${sections.join("\n\n---\n\n")}`;
2334
- }
2543
+ };
2335
2544
 
2336
2545
  // src/installers/nextjs.ts
2546
+ import { join as join2 } from "path";
2337
2547
  var VexNextJSInstaller = class extends VexFrameworkInstaller {
2338
2548
  /**
2339
2549
  * Framework identifier used for template selection and logging.