create-payloadpack-auth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +633 -0
- package/package.json +51 -0
package/bin/cli.js
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
|
|
5
|
+
// src/constants.ts
|
|
6
|
+
var REGISTRY_URL = "https://payloadpack.nodejs.pub";
|
|
7
|
+
var PACKAGE_NAME = "@payloadpack/auth";
|
|
8
|
+
var DOCS_URL = "https://better-payload-auth.com/docs";
|
|
9
|
+
var TOKEN_ENV_VAR = "PAYLOADPACK_AUTH_TOKEN";
|
|
10
|
+
var DEPENDENCIES = ["@payloadpack/auth", "better-auth"];
|
|
11
|
+
var DEV_DEPENDENCIES = ["dotenv-cli"];
|
|
12
|
+
var NPMRC_CONTENT = `@payloadpack:registry=https://payloadpack.nodejs.pub
|
|
13
|
+
//payloadpack.nodejs.pub/:_authToken=\${PAYLOADPACK_AUTH_TOKEN}
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
// src/validators/license.ts
|
|
17
|
+
async function validateLicenseKey(token) {
|
|
18
|
+
try {
|
|
19
|
+
const encodedPackageName = PACKAGE_NAME.replace("/", "%2F");
|
|
20
|
+
const response = await fetch(`${REGISTRY_URL}/${encodedPackageName}`, {
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${token}`
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
if (response.status === 401 || response.status === 403) {
|
|
26
|
+
return { valid: false, error: "Invalid or expired license key" };
|
|
27
|
+
}
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
return { valid: false, error: `Registry returned status ${response.status}` };
|
|
30
|
+
}
|
|
31
|
+
return { valid: true };
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return {
|
|
34
|
+
valid: false,
|
|
35
|
+
error: error instanceof Error ? error.message : "Network error validating license"
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/validators/project.ts
|
|
41
|
+
import fs from "fs-extra";
|
|
42
|
+
import path from "path";
|
|
43
|
+
async function validatePayloadProject(cwd) {
|
|
44
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
45
|
+
if (!await fs.pathExists(pkgPath)) {
|
|
46
|
+
return { valid: false, srcDir: "src", configPath: "", error: "No package.json found" };
|
|
47
|
+
}
|
|
48
|
+
const pkg = await fs.readJson(pkgPath);
|
|
49
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
50
|
+
if (!deps["payload"]) {
|
|
51
|
+
return { valid: false, srcDir: "src", configPath: "", error: "Payload is not installed" };
|
|
52
|
+
}
|
|
53
|
+
const possibleConfigs = [
|
|
54
|
+
{ path: "src/payload.config.ts", srcDir: "src" },
|
|
55
|
+
{ path: "payload.config.ts", srcDir: "." }
|
|
56
|
+
];
|
|
57
|
+
for (const config of possibleConfigs) {
|
|
58
|
+
const fullPath = path.join(cwd, config.path);
|
|
59
|
+
if (await fs.pathExists(fullPath)) {
|
|
60
|
+
return {
|
|
61
|
+
valid: true,
|
|
62
|
+
srcDir: config.srcDir,
|
|
63
|
+
configPath: fullPath
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
valid: false,
|
|
69
|
+
srcDir: "src",
|
|
70
|
+
configPath: "",
|
|
71
|
+
error: "Could not find payload.config.ts"
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/validators/package-manager.ts
|
|
76
|
+
import fs2 from "fs-extra";
|
|
77
|
+
import path2 from "path";
|
|
78
|
+
var LOCK_FILES = {
|
|
79
|
+
"bun.lock": "bun",
|
|
80
|
+
"bun.lockb": "bun",
|
|
81
|
+
"pnpm-lock.yaml": "pnpm",
|
|
82
|
+
"yarn.lock": "yarn",
|
|
83
|
+
"package-lock.json": "npm"
|
|
84
|
+
};
|
|
85
|
+
var ADD_COMMANDS = {
|
|
86
|
+
bun: "bun add",
|
|
87
|
+
pnpm: "pnpm add",
|
|
88
|
+
yarn: "yarn add",
|
|
89
|
+
npm: "npm install"
|
|
90
|
+
};
|
|
91
|
+
async function detectPackageManager(cwd) {
|
|
92
|
+
for (const [lockfile, pm] of Object.entries(LOCK_FILES)) {
|
|
93
|
+
const lockfilePath = path2.join(cwd, lockfile);
|
|
94
|
+
if (await fs2.pathExists(lockfilePath)) {
|
|
95
|
+
return { name: pm, addCommand: ADD_COMMANDS[pm] };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const pkgPath = path2.join(cwd, "package.json");
|
|
99
|
+
if (await fs2.pathExists(pkgPath)) {
|
|
100
|
+
const pkg = await fs2.readJson(pkgPath);
|
|
101
|
+
if (pkg.packageManager) {
|
|
102
|
+
const match = pkg.packageManager.match(/^(npm|yarn|pnpm|bun)@/);
|
|
103
|
+
if (match && match[1] in ADD_COMMANDS) {
|
|
104
|
+
const name = match[1];
|
|
105
|
+
return { name, addCommand: ADD_COMMANDS[name] };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { name: "npm", addCommand: ADD_COMMANDS.npm };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/scaffolders/files.ts
|
|
113
|
+
import fs3 from "fs-extra";
|
|
114
|
+
import path3 from "path";
|
|
115
|
+
|
|
116
|
+
// src/templates/index.ts
|
|
117
|
+
var TEMPLATES = {
|
|
118
|
+
"lib/auth/options.ts": `import { nextCookies } from 'better-auth/next-js'
|
|
119
|
+
import type { BetterAuthOptionsFn } from '@payloadpack/auth/plugin'
|
|
120
|
+
|
|
121
|
+
const baseURL = process.env.NEXT_PUBLIC_BETTER_AUTH_URL ?? 'http://localhost:3000'
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Better Auth options as a function that receives the Payload instance.
|
|
125
|
+
* The payload object is only available in callbacks since these are called at request time.
|
|
126
|
+
* Alternatively, you can use \`const payload = await getPayload()\` inside callbacks.
|
|
127
|
+
*/
|
|
128
|
+
export const betterAuthOptions = ((payload) => ({
|
|
129
|
+
baseURL,
|
|
130
|
+
trustedOrigins: [baseURL],
|
|
131
|
+
secret: process.env.PAYLOAD_SECRET,
|
|
132
|
+
plugins: [nextCookies()],
|
|
133
|
+
emailAndPassword: {
|
|
134
|
+
enabled: true,
|
|
135
|
+
requireEmailVerification: true,
|
|
136
|
+
async sendResetPassword({ user, url }) {
|
|
137
|
+
if (!payload) throw new Error('Payload not available in sendResetPassword callback')
|
|
138
|
+
// Don't await to prevent timing attacks
|
|
139
|
+
void payload.sendEmail({
|
|
140
|
+
to: user.email,
|
|
141
|
+
subject: 'Reset your password',
|
|
142
|
+
text: \`Click here to reset your password: \${url}\`
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
emailVerification: {
|
|
147
|
+
sendOnSignUp: true,
|
|
148
|
+
autoSignInAfterVerification: true,
|
|
149
|
+
async sendVerificationEmail({ user, url }) {
|
|
150
|
+
if (!payload) throw new Error('Payload not available in sendVerificationEmail callback')
|
|
151
|
+
// Don't await to prevent timing attacks
|
|
152
|
+
void payload.sendEmail({
|
|
153
|
+
to: user.email,
|
|
154
|
+
subject: 'Verify your email',
|
|
155
|
+
text: \`Click here to verify your email: \${url}\`
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
})) satisfies BetterAuthOptionsFn
|
|
160
|
+
`,
|
|
161
|
+
"lib/auth/plugin-options.ts": `import type { BetterAuthPluginOptionsInput } from '@payloadpack/auth/plugin'
|
|
162
|
+
import { betterAuthOptions } from './options'
|
|
163
|
+
|
|
164
|
+
export const betterAuthPluginOptions = {
|
|
165
|
+
betterAuthOptions
|
|
166
|
+
} satisfies BetterAuthPluginOptionsInput
|
|
167
|
+
`,
|
|
168
|
+
"lib/auth/types.ts": `import type { betterAuth } from 'better-auth'
|
|
169
|
+
import type { betterAuthOptions } from './options'
|
|
170
|
+
|
|
171
|
+
/** Used to type payload.betterAuth with your auth configuration. */
|
|
172
|
+
export type Auth = ReturnType<typeof betterAuth<ReturnType<typeof betterAuthOptions>>>
|
|
173
|
+
export type Session = Auth['$Infer']['Session']
|
|
174
|
+
export type User = Auth['$Infer']['Session']['user']
|
|
175
|
+
`,
|
|
176
|
+
"lib/get-payload.ts": `import 'server-only'
|
|
177
|
+
|
|
178
|
+
import { getPayload as getPayloadBase } from 'payload'
|
|
179
|
+
import { withBetterAuth } from '@payloadpack/auth/plugin'
|
|
180
|
+
import payloadConfig from '@payload-config'
|
|
181
|
+
import type { Auth } from './auth/types'
|
|
182
|
+
|
|
183
|
+
export const getPayload = withBetterAuth<Auth>(async () => {
|
|
184
|
+
return await getPayloadBase({ config: payloadConfig })
|
|
185
|
+
})
|
|
186
|
+
`,
|
|
187
|
+
"app/api/auth/[...all]/route.ts": `import configPromise from '@payload-config'
|
|
188
|
+
import { toNextJsHandler } from '@payloadpack/auth/next-js'
|
|
189
|
+
|
|
190
|
+
export const { POST, GET } = toNextJsHandler(configPromise)
|
|
191
|
+
`
|
|
192
|
+
};
|
|
193
|
+
var TEMPLATE_PATHS = Object.keys(TEMPLATES);
|
|
194
|
+
|
|
195
|
+
// src/scaffolders/files.ts
|
|
196
|
+
async function scaffoldFiles(cwd, srcDir) {
|
|
197
|
+
const created = [];
|
|
198
|
+
const skipped = [];
|
|
199
|
+
for (const [relativePath, content] of Object.entries(TEMPLATES)) {
|
|
200
|
+
const fullPath = path3.join(cwd, srcDir, relativePath);
|
|
201
|
+
if (await fs3.pathExists(fullPath)) {
|
|
202
|
+
skipped.push(relativePath);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
await fs3.ensureDir(path3.dirname(fullPath));
|
|
206
|
+
await fs3.writeFile(fullPath, content, "utf-8");
|
|
207
|
+
created.push(relativePath);
|
|
208
|
+
}
|
|
209
|
+
return { created, skipped };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/scaffolders/npmrc.ts
|
|
213
|
+
import fs4 from "fs-extra";
|
|
214
|
+
import path4 from "path";
|
|
215
|
+
async function configureNpmrc(cwd) {
|
|
216
|
+
const npmrcPath = path4.join(cwd, ".npmrc");
|
|
217
|
+
if (await fs4.pathExists(npmrcPath)) {
|
|
218
|
+
const existing = await fs4.readFile(npmrcPath, "utf-8");
|
|
219
|
+
const hasRegistry = existing.includes("@payloadpack:registry");
|
|
220
|
+
const hasTokenLine = existing.includes("//payloadpack.nodejs.pub/:_authToken");
|
|
221
|
+
if (hasRegistry && hasTokenLine) {
|
|
222
|
+
return { created: false, updated: false, alreadyConfigured: true };
|
|
223
|
+
}
|
|
224
|
+
const newContent = existing.endsWith("\n") ? existing + NPMRC_CONTENT : existing + "\n" + NPMRC_CONTENT;
|
|
225
|
+
await fs4.writeFile(npmrcPath, newContent, "utf-8");
|
|
226
|
+
return { created: false, updated: true, alreadyConfigured: false };
|
|
227
|
+
}
|
|
228
|
+
await fs4.writeFile(npmrcPath, NPMRC_CONTENT, "utf-8");
|
|
229
|
+
return { created: true, updated: false, alreadyConfigured: false };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/scaffolders/env.ts
|
|
233
|
+
import fs5 from "fs-extra";
|
|
234
|
+
import path5 from "path";
|
|
235
|
+
async function configureEnvFile(cwd, token) {
|
|
236
|
+
const envPath = path5.join(cwd, ".env");
|
|
237
|
+
if (await fs5.pathExists(envPath)) {
|
|
238
|
+
const existing = await fs5.readFile(envPath, "utf-8");
|
|
239
|
+
const tokenRegex = new RegExp(`^${TOKEN_ENV_VAR}=.*$`, "m");
|
|
240
|
+
if (tokenRegex.test(existing)) {
|
|
241
|
+
const newContent2 = existing.replace(tokenRegex, `${TOKEN_ENV_VAR}=${token}`);
|
|
242
|
+
await fs5.writeFile(envPath, newContent2, "utf-8");
|
|
243
|
+
return { created: false, updated: true };
|
|
244
|
+
}
|
|
245
|
+
const newContent = existing.endsWith("\n") ? `${existing}${TOKEN_ENV_VAR}=${token}
|
|
246
|
+
` : `${existing}
|
|
247
|
+
${TOKEN_ENV_VAR}=${token}
|
|
248
|
+
`;
|
|
249
|
+
await fs5.writeFile(envPath, newContent, "utf-8");
|
|
250
|
+
return { created: false, updated: true };
|
|
251
|
+
}
|
|
252
|
+
await fs5.writeFile(envPath, `${TOKEN_ENV_VAR}=${token}
|
|
253
|
+
`, "utf-8");
|
|
254
|
+
return { created: true, updated: false };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/scaffolders/package-json.ts
|
|
258
|
+
import fs6 from "fs-extra";
|
|
259
|
+
import path6 from "path";
|
|
260
|
+
var INSTALL_SCRIPT_NAME = "install:with-keys";
|
|
261
|
+
var INSTALL_COMMANDS = {
|
|
262
|
+
bun: "dotenv -- bun install",
|
|
263
|
+
pnpm: "dotenv -- pnpm install",
|
|
264
|
+
yarn: "dotenv -- yarn install",
|
|
265
|
+
npm: "dotenv -- npm install"
|
|
266
|
+
};
|
|
267
|
+
async function addInstallScript(cwd, pm) {
|
|
268
|
+
const pkgPath = path6.join(cwd, "package.json");
|
|
269
|
+
const content = await fs6.readFile(pkgPath, "utf-8");
|
|
270
|
+
const pkg = JSON.parse(content);
|
|
271
|
+
pkg.scripts ??= {};
|
|
272
|
+
if (pkg.scripts[INSTALL_SCRIPT_NAME]) {
|
|
273
|
+
return { added: false, alreadyExists: true };
|
|
274
|
+
}
|
|
275
|
+
pkg.scripts[INSTALL_SCRIPT_NAME] = INSTALL_COMMANDS[pm];
|
|
276
|
+
await fs6.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
277
|
+
return { added: true, alreadyExists: false };
|
|
278
|
+
}
|
|
279
|
+
function getInstallScriptName() {
|
|
280
|
+
return INSTALL_SCRIPT_NAME;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/scaffolders/payload-config.ts
|
|
284
|
+
import fs7 from "fs-extra";
|
|
285
|
+
import path7 from "path";
|
|
286
|
+
import { parseModule, generateCode, builders } from "magicast";
|
|
287
|
+
import { execa } from "execa";
|
|
288
|
+
async function detectFormatter(cwd) {
|
|
289
|
+
const biomeConfigs = ["biome.json", "biome.jsonc"];
|
|
290
|
+
for (const config of biomeConfigs) {
|
|
291
|
+
if (await fs7.pathExists(path7.join(cwd, config))) {
|
|
292
|
+
return "biome";
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const prettierConfigs = [
|
|
296
|
+
".prettierrc",
|
|
297
|
+
".prettierrc.json",
|
|
298
|
+
".prettierrc.js",
|
|
299
|
+
".prettierrc.cjs",
|
|
300
|
+
".prettierrc.mjs",
|
|
301
|
+
".prettierrc.yaml",
|
|
302
|
+
".prettierrc.yml",
|
|
303
|
+
"prettier.config.js",
|
|
304
|
+
"prettier.config.cjs",
|
|
305
|
+
"prettier.config.mjs"
|
|
306
|
+
];
|
|
307
|
+
for (const config of prettierConfigs) {
|
|
308
|
+
if (await fs7.pathExists(path7.join(cwd, config))) {
|
|
309
|
+
return "prettier";
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const pkgPath = path7.join(cwd, "package.json");
|
|
313
|
+
if (await fs7.pathExists(pkgPath)) {
|
|
314
|
+
const pkg = await fs7.readJson(pkgPath);
|
|
315
|
+
if (pkg.prettier) {
|
|
316
|
+
return "prettier";
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
async function detectPackageRunner(cwd) {
|
|
322
|
+
if (await fs7.pathExists(path7.join(cwd, "bun.lock")) || await fs7.pathExists(path7.join(cwd, "bun.lockb"))) {
|
|
323
|
+
return "bunx";
|
|
324
|
+
}
|
|
325
|
+
return "npx";
|
|
326
|
+
}
|
|
327
|
+
async function formatFile(filePath, formatter, cwd) {
|
|
328
|
+
if (!formatter) return false;
|
|
329
|
+
const runner = await detectPackageRunner(cwd);
|
|
330
|
+
try {
|
|
331
|
+
if (formatter === "biome") {
|
|
332
|
+
await execa(runner, ["@biomejs/biome", "format", "--write", filePath], { cwd, stdio: "pipe" });
|
|
333
|
+
} else {
|
|
334
|
+
await execa(runner, ["prettier", "--write", filePath], { cwd, stdio: "pipe" });
|
|
335
|
+
}
|
|
336
|
+
return true;
|
|
337
|
+
} catch {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
var MANUAL_INSTRUCTIONS = `
|
|
342
|
+
Add these imports at the top of your payload.config.ts:
|
|
343
|
+
|
|
344
|
+
import { betterPayloadAuth } from '@payloadpack/auth'
|
|
345
|
+
import { betterAuthPluginOptions } from './lib/auth/plugin-options'
|
|
346
|
+
|
|
347
|
+
Then add the plugin to your buildConfig plugins array:
|
|
348
|
+
|
|
349
|
+
plugins: [betterPayloadAuth(betterAuthPluginOptions)]
|
|
350
|
+
`;
|
|
351
|
+
function getManualInstructions() {
|
|
352
|
+
return MANUAL_INSTRUCTIONS;
|
|
353
|
+
}
|
|
354
|
+
async function modifyPayloadConfig(configPath, cwd) {
|
|
355
|
+
const source = await fs7.readFile(configPath, "utf-8");
|
|
356
|
+
if (source.includes("betterPayloadAuth") || source.includes("better-payload-auth")) {
|
|
357
|
+
return { success: true, modified: false, alreadyConfigured: true };
|
|
358
|
+
}
|
|
359
|
+
const formatter = await detectFormatter(cwd);
|
|
360
|
+
const backupPath = `${configPath}.backup-${Date.now()}`;
|
|
361
|
+
await fs7.copyFile(configPath, backupPath);
|
|
362
|
+
try {
|
|
363
|
+
const mod = parseModule(source);
|
|
364
|
+
mod.imports.$prepend({
|
|
365
|
+
from: "@payloadpack/auth",
|
|
366
|
+
imported: "betterPayloadAuth"
|
|
367
|
+
});
|
|
368
|
+
mod.imports.$prepend({
|
|
369
|
+
from: "./lib/auth/plugin-options",
|
|
370
|
+
imported: "betterAuthPluginOptions"
|
|
371
|
+
});
|
|
372
|
+
const configObj = mod.exports.default?.$args?.[0];
|
|
373
|
+
if (!configObj) {
|
|
374
|
+
throw new Error("Could not find buildConfig object");
|
|
375
|
+
}
|
|
376
|
+
const existingPlugins = configObj.plugins;
|
|
377
|
+
if (existingPlugins && typeof existingPlugins.length === "number") {
|
|
378
|
+
existingPlugins.unshift(builders.raw("betterPayloadAuth(betterAuthPluginOptions)"));
|
|
379
|
+
} else if (!existingPlugins) {
|
|
380
|
+
configObj.plugins = builders.raw("[betterPayloadAuth(betterAuthPluginOptions)]");
|
|
381
|
+
} else {
|
|
382
|
+
throw new Error("plugins property exists but is not an array literal - cannot safely modify");
|
|
383
|
+
}
|
|
384
|
+
const result = generateCode(mod).code;
|
|
385
|
+
await fs7.writeFile(configPath, result, "utf-8");
|
|
386
|
+
const formatted = await formatFile(configPath, formatter, cwd);
|
|
387
|
+
await fs7.remove(backupPath);
|
|
388
|
+
return { success: true, modified: true, formatted };
|
|
389
|
+
} catch (error) {
|
|
390
|
+
await fs7.copyFile(backupPath, configPath);
|
|
391
|
+
await fs7.remove(backupPath);
|
|
392
|
+
return {
|
|
393
|
+
success: false,
|
|
394
|
+
modified: false,
|
|
395
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
396
|
+
manualRequired: true
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/scaffolders/dockerfile.ts
|
|
402
|
+
import fs8 from "fs-extra";
|
|
403
|
+
import path8 from "path";
|
|
404
|
+
var INSTALL_CHECK_PATTERNS = {
|
|
405
|
+
bun: /^\s*RUN\s+bun\s+(?:install|i)\b/m,
|
|
406
|
+
pnpm: /^\s*RUN\s+pnpm\s+(?:install|i)\b/m,
|
|
407
|
+
yarn: /^\s*RUN\s+yarn(?:\s+(?:install|i)\b|\s+--|\s*$)/m,
|
|
408
|
+
npm: /^\s*RUN\s+npm\s+(?:ci|install|i)\b/m
|
|
409
|
+
};
|
|
410
|
+
var INSTALL_LINE_PATTERNS = {
|
|
411
|
+
bun: /^(\s*)RUN\s+(bun\s+(?:install|i)\b.*)$/m,
|
|
412
|
+
pnpm: /^(\s*)RUN\s+(pnpm\s+(?:install|i)\b.*)$/m,
|
|
413
|
+
yarn: /^(\s*)RUN\s+(yarn(?:\s+(?:install|i)\b|\s+--)?.*)$/m,
|
|
414
|
+
npm: /^(\s*)RUN\s+(npm\s+(?:ci|install|i)\b.*)$/m
|
|
415
|
+
};
|
|
416
|
+
var SECRET_MOUNT_PATTERN = /--mount=type=secret,id=BETTER_PAYLOAD_AUTH_TOKEN/;
|
|
417
|
+
async function checkDockerfile(cwd, pm) {
|
|
418
|
+
const dockerfilePath = path8.join(cwd, "Dockerfile");
|
|
419
|
+
if (!await fs8.pathExists(dockerfilePath)) {
|
|
420
|
+
return { hasDockerfile: false, hasInstallCommand: false, dockerfilePath: null };
|
|
421
|
+
}
|
|
422
|
+
const content = await fs8.readFile(dockerfilePath, "utf-8");
|
|
423
|
+
const pattern = INSTALL_CHECK_PATTERNS[pm];
|
|
424
|
+
const hasInstallCommand = pattern.test(content);
|
|
425
|
+
return { hasDockerfile: true, hasInstallCommand, dockerfilePath };
|
|
426
|
+
}
|
|
427
|
+
async function modifyDockerfile(dockerfilePath, pm) {
|
|
428
|
+
const content = await fs8.readFile(dockerfilePath, "utf-8");
|
|
429
|
+
if (SECRET_MOUNT_PATTERN.test(content)) {
|
|
430
|
+
return { modified: false, alreadyConfigured: true };
|
|
431
|
+
}
|
|
432
|
+
const pattern = INSTALL_LINE_PATTERNS[pm];
|
|
433
|
+
const newContent = content.replace(pattern, (_match, indent, originalCommand) => {
|
|
434
|
+
return `${indent}RUN --mount=type=secret,id=${TOKEN_ENV_VAR},env=${TOKEN_ENV_VAR} \\
|
|
435
|
+
${indent} ${originalCommand}`;
|
|
436
|
+
});
|
|
437
|
+
await fs8.writeFile(dockerfilePath, newContent, "utf-8");
|
|
438
|
+
return { modified: true, alreadyConfigured: false };
|
|
439
|
+
}
|
|
440
|
+
var EXAMPLE_INSTALL_COMMANDS = {
|
|
441
|
+
bun: "bun install --frozen-lockfile",
|
|
442
|
+
pnpm: "pnpm install --frozen-lockfile",
|
|
443
|
+
yarn: "yarn install --frozen-lockfile",
|
|
444
|
+
npm: "npm ci"
|
|
445
|
+
};
|
|
446
|
+
function getDockerfileInstructions(pm) {
|
|
447
|
+
const exampleCommand = EXAMPLE_INSTALL_COMMANDS[pm];
|
|
448
|
+
return `Add the secret mount to your install command:
|
|
449
|
+
|
|
450
|
+
RUN --mount=type=secret,id=${TOKEN_ENV_VAR},env=${TOKEN_ENV_VAR} \\
|
|
451
|
+
${exampleCommand}
|
|
452
|
+
|
|
453
|
+
Then build with:
|
|
454
|
+
docker build --secret id=${TOKEN_ENV_VAR},env=${TOKEN_ENV_VAR} .`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/utils/exec.ts
|
|
458
|
+
import { execa as execa2 } from "execa";
|
|
459
|
+
async function installDependencies({
|
|
460
|
+
cwd,
|
|
461
|
+
pm,
|
|
462
|
+
packages,
|
|
463
|
+
licenseKey,
|
|
464
|
+
dev = false
|
|
465
|
+
}) {
|
|
466
|
+
let args;
|
|
467
|
+
if (pm.name === "npm") {
|
|
468
|
+
args = ["install", ...dev ? ["--save-dev"] : [], ...packages];
|
|
469
|
+
} else {
|
|
470
|
+
args = ["add", ...dev ? ["-D"] : [], ...packages];
|
|
471
|
+
}
|
|
472
|
+
await execa2(pm.name, args, {
|
|
473
|
+
cwd,
|
|
474
|
+
stdio: "inherit",
|
|
475
|
+
env: {
|
|
476
|
+
...process.env,
|
|
477
|
+
// Set token for the install command to authenticate with registry
|
|
478
|
+
[TOKEN_ENV_VAR]: licenseKey
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/index.ts
|
|
484
|
+
async function main() {
|
|
485
|
+
const cwd = process.cwd();
|
|
486
|
+
console.log();
|
|
487
|
+
p.intro(pc.bgCyan(pc.black(" Better Payload Auth Setup ")));
|
|
488
|
+
const project = await validatePayloadProject(cwd);
|
|
489
|
+
if (!project.valid) {
|
|
490
|
+
p.cancel(project.error || "This does not appear to be a Payload CMS project.");
|
|
491
|
+
p.outro(pc.dim("Make sure you run this command from a directory with a payload.config.ts file."));
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
const licenseKey = await p.text({
|
|
495
|
+
message: "Enter your Anystack license key:"
|
|
496
|
+
});
|
|
497
|
+
if (p.isCancel(licenseKey)) {
|
|
498
|
+
p.cancel("Setup cancelled.");
|
|
499
|
+
process.exit(0);
|
|
500
|
+
}
|
|
501
|
+
const spinner2 = p.spinner();
|
|
502
|
+
spinner2.start("Validating license...");
|
|
503
|
+
const license = await validateLicenseKey(licenseKey);
|
|
504
|
+
if (!license.valid) {
|
|
505
|
+
spinner2.stop("License validation failed");
|
|
506
|
+
p.cancel(license.error || "Invalid license key");
|
|
507
|
+
p.outro(pc.dim("Get your license key from https://anystack.sh after purchasing."));
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
spinner2.stop("License valid!");
|
|
511
|
+
const pm = await detectPackageManager(cwd);
|
|
512
|
+
p.log.info(`Detected package manager: ${pc.cyan(pm.name)}`);
|
|
513
|
+
const dockerCheck = await checkDockerfile(cwd, pm.name);
|
|
514
|
+
const planLines = [
|
|
515
|
+
"This will:",
|
|
516
|
+
...TEMPLATE_PATHS.map((f) => ` ${pc.green("+")} Create ${f}`),
|
|
517
|
+
` ${pc.yellow("~")} Update/create .npmrc with registry configuration`,
|
|
518
|
+
` ${pc.yellow("~")} Add ${TOKEN_ENV_VAR} to .env`,
|
|
519
|
+
` ${pc.yellow("~")} Add "${getInstallScriptName()}" script to package.json`,
|
|
520
|
+
` ${pc.yellow("~")} Modify payload.config.ts to add the plugin`,
|
|
521
|
+
...dockerCheck.hasDockerfile && dockerCheck.hasInstallCommand ? [` ${pc.yellow("?")} Optionally configure Dockerfile for BuildKit secrets`] : [],
|
|
522
|
+
` ${pc.blue(">")} Install ${DEPENDENCIES.join(", ")} and ${DEV_DEPENDENCIES.join(", ")}`
|
|
523
|
+
];
|
|
524
|
+
p.note(planLines.join("\n"), "Setup Plan");
|
|
525
|
+
const proceed = await p.confirm({
|
|
526
|
+
message: "Proceed with setup?",
|
|
527
|
+
initialValue: true
|
|
528
|
+
});
|
|
529
|
+
if (!proceed || p.isCancel(proceed)) {
|
|
530
|
+
p.cancel("Setup cancelled.");
|
|
531
|
+
process.exit(0);
|
|
532
|
+
}
|
|
533
|
+
spinner2.start("Creating auth configuration files...");
|
|
534
|
+
const scaffoldResult = await scaffoldFiles(cwd, project.srcDir);
|
|
535
|
+
spinner2.stop(`Created ${scaffoldResult.created.length} files`);
|
|
536
|
+
if (scaffoldResult.skipped.length > 0) {
|
|
537
|
+
p.log.warn(`Skipped ${scaffoldResult.skipped.length} existing files: ${scaffoldResult.skipped.join(", ")}`);
|
|
538
|
+
}
|
|
539
|
+
spinner2.start("Configuring .npmrc...");
|
|
540
|
+
const npmrcResult = await configureNpmrc(cwd);
|
|
541
|
+
spinner2.stop(
|
|
542
|
+
npmrcResult.alreadyConfigured ? ".npmrc already configured" : npmrcResult.created ? "Created .npmrc" : "Updated .npmrc"
|
|
543
|
+
);
|
|
544
|
+
spinner2.start(`Adding ${TOKEN_ENV_VAR} to .env...`);
|
|
545
|
+
const envResult = await configureEnvFile(cwd, licenseKey);
|
|
546
|
+
spinner2.stop(envResult.created ? `Created .env with ${TOKEN_ENV_VAR}` : `Updated ${TOKEN_ENV_VAR} in .env`);
|
|
547
|
+
spinner2.start("Adding install script to package.json...");
|
|
548
|
+
const scriptResult = await addInstallScript(cwd, pm.name);
|
|
549
|
+
spinner2.stop(
|
|
550
|
+
scriptResult.alreadyExists ? `"${getInstallScriptName()}" script already exists` : `Added "${getInstallScriptName()}" script to package.json`
|
|
551
|
+
);
|
|
552
|
+
spinner2.start("Updating payload.config.ts...");
|
|
553
|
+
const configResult = await modifyPayloadConfig(project.configPath, cwd);
|
|
554
|
+
if (configResult.success) {
|
|
555
|
+
let message;
|
|
556
|
+
if (configResult.alreadyConfigured) {
|
|
557
|
+
message = "payload.config.ts already configured";
|
|
558
|
+
} else if (configResult.formatted) {
|
|
559
|
+
message = "Updated payload.config.ts (formatted)";
|
|
560
|
+
} else {
|
|
561
|
+
message = "Updated payload.config.ts";
|
|
562
|
+
}
|
|
563
|
+
spinner2.stop(message);
|
|
564
|
+
} else {
|
|
565
|
+
spinner2.stop("Could not automatically update payload.config.ts");
|
|
566
|
+
p.log.warn("Please manually update your payload.config.ts:");
|
|
567
|
+
p.note(getManualInstructions(), "Manual Steps Required");
|
|
568
|
+
}
|
|
569
|
+
if (dockerCheck.hasDockerfile && dockerCheck.hasInstallCommand && dockerCheck.dockerfilePath) {
|
|
570
|
+
const modifyDocker = await p.confirm({
|
|
571
|
+
message: "Dockerfile detected with install command. Configure it to use BuildKit secrets for registry auth?",
|
|
572
|
+
initialValue: true
|
|
573
|
+
});
|
|
574
|
+
if (!p.isCancel(modifyDocker) && modifyDocker) {
|
|
575
|
+
spinner2.start("Updating Dockerfile...");
|
|
576
|
+
const dockerResult = await modifyDockerfile(dockerCheck.dockerfilePath, pm.name);
|
|
577
|
+
spinner2.stop(
|
|
578
|
+
dockerResult.alreadyConfigured ? "Dockerfile already configured for secrets" : "Updated Dockerfile with BuildKit secret mount"
|
|
579
|
+
);
|
|
580
|
+
} else if (!p.isCancel(modifyDocker)) {
|
|
581
|
+
p.log.info("Skipped Dockerfile modification");
|
|
582
|
+
p.note(getDockerfileInstructions(pm.name), "Manual Dockerfile Setup");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
p.log.step("Installing dependencies...");
|
|
586
|
+
console.log();
|
|
587
|
+
try {
|
|
588
|
+
await installDependencies({
|
|
589
|
+
cwd,
|
|
590
|
+
pm,
|
|
591
|
+
packages: [...DEPENDENCIES],
|
|
592
|
+
licenseKey
|
|
593
|
+
});
|
|
594
|
+
await installDependencies({
|
|
595
|
+
cwd,
|
|
596
|
+
pm,
|
|
597
|
+
packages: [...DEV_DEPENDENCIES],
|
|
598
|
+
licenseKey,
|
|
599
|
+
dev: true
|
|
600
|
+
});
|
|
601
|
+
console.log();
|
|
602
|
+
p.log.success("Dependencies installed");
|
|
603
|
+
} catch {
|
|
604
|
+
console.log();
|
|
605
|
+
p.log.error("Failed to install dependencies");
|
|
606
|
+
p.log.warn(
|
|
607
|
+
`Run manually: ${pc.cyan(`${TOKEN_ENV_VAR}=<your-token> ${pm.addCommand} ${DEPENDENCIES.join(" ")}`)}`
|
|
608
|
+
);
|
|
609
|
+
p.outro(pc.red("Setup incomplete - dependency installation failed"));
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
p.outro(pc.green("Better Payload Auth is now configured!"));
|
|
613
|
+
console.log(`
|
|
614
|
+
${pc.bold("Next steps:")}
|
|
615
|
+
1. Ensure ${pc.cyan("PAYLOAD_SECRET")} is set in your .env (required for auth)
|
|
616
|
+
2. Run your dev server: ${pc.cyan(`${pm.name} run dev`)}
|
|
617
|
+
3. Visit ${pc.cyan("/admin")} to see the new auth UI
|
|
618
|
+
|
|
619
|
+
${pc.bold("Future installs:")}
|
|
620
|
+
Use ${pc.cyan(`${pm.name} run ${getInstallScriptName()}`)} to install dependencies with registry auth.
|
|
621
|
+
This reads ${TOKEN_ENV_VAR} from your .env file via dotenv-cli.
|
|
622
|
+
|
|
623
|
+
${pc.yellow("Note:")} The .npmrc references ${pc.cyan("${" + TOKEN_ENV_VAR + "}")} - safe to commit.
|
|
624
|
+
Your .env file contains the actual token - ${pc.bold("do not commit it")}.
|
|
625
|
+
|
|
626
|
+
${pc.dim(`Docs: ${DOCS_URL}`)}
|
|
627
|
+
`);
|
|
628
|
+
}
|
|
629
|
+
main().catch((error) => {
|
|
630
|
+
p.cancel("An unexpected error occurred");
|
|
631
|
+
console.error(error);
|
|
632
|
+
process.exit(1);
|
|
633
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-payloadpack-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI to set up @payloadpack/auth in Payload CMS projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-payloadpack-auth": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
16
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
17
|
+
"test": "bun test",
|
|
18
|
+
"prepublishOnly": "bun run build"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@clack/prompts": "^0.8.2",
|
|
22
|
+
"execa": "^9.5.2",
|
|
23
|
+
"fs-extra": "^11.3.0",
|
|
24
|
+
"magicast": "^0.3.5",
|
|
25
|
+
"picocolors": "^1.1.1"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/fs-extra": "^11.0.4",
|
|
29
|
+
"@types/node": "^22.5.4",
|
|
30
|
+
"tsup": "^8.4.0",
|
|
31
|
+
"typescript": "5.7.3"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22.0.0"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/payloadpack/auth.git",
|
|
39
|
+
"directory": "packages/create-payloadpack-auth"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"payload",
|
|
43
|
+
"payloadcms",
|
|
44
|
+
"better-auth",
|
|
45
|
+
"authentication",
|
|
46
|
+
"cli",
|
|
47
|
+
"create"
|
|
48
|
+
],
|
|
49
|
+
"author": "Payload Pack",
|
|
50
|
+
"license": "MIT"
|
|
51
|
+
}
|