g1t/services/identity/scripts/create-user.mjs
Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Initial g1t: services, event bus, intents and attempts | 1 | // Creates a user (or resets their password) in the remote D1 database and |
| 2 | // writes the generated password to the gitignored .credentials directory. | |
| 3 | // | |
| 4 | // node scripts/create-user.mjs <username> [email] | |
| 5 | import { execSync } from "node:child_process"; | |
| 6 | import { pbkdf2Sync, randomBytes } from "node:crypto"; | |
| 7 | import { mkdirSync, rmSync, writeFileSync } from "node:fs"; | |
| 8 | import { tmpdir } from "node:os"; | |
| 9 | import { join } from "node:path"; | |
| 10 | ||
| 11 | const [username, email] = process.argv.slice(2); | |
| 12 | if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/.test(username ?? "")) { | |
| 13 | console.error("usage: node scripts/create-user.mjs <username> [email]"); | |
| 14 | process.exit(1); | |
| 15 | } | |
| 16 | if (email && !/^[^\s'@]+@[^\s'@]+$/.test(email)) { | |
| 17 | console.error("invalid email"); | |
| 18 | process.exit(1); | |
| 19 | } | |
| 20 | ||
| 21 | // Must match hashPassword in src/crypto.ts. | |
| 22 | const iterations = 100_000; | |
| 23 | const password = randomBytes(18).toString("base64url"); | |
| 24 | const salt = randomBytes(16); | |
| 25 | const hash = pbkdf2Sync(password, salt, iterations, 32, "sha256"); | |
| 26 | const stored = `pbkdf2$${iterations}$${salt.toString("base64")}$${hash.toString("base64")}`; | |
| 27 | ||
| 28 | const sqlFile = join(tmpdir(), `g1t-create-user-${process.pid}.sql`); | |
| 29 | writeFileSync( | |
| 30 | sqlFile, | |
| 31 | `INSERT INTO users (id, username, email, password_hash) | |
| 32 | VALUES ('usr_' || lower(hex(randomblob(13))), '${username}', ${email ? `'${email}'` : "NULL"}, '${stored}') | |
| 33 | ON CONFLICT (username) DO UPDATE SET password_hash = excluded.password_hash;`, | |
| 34 | ); | |
| 35 | try { | |
| 36 | execSync(`npx wrangler d1 execute g1t --remote --yes --file "${sqlFile}"`, { | |
| 37 | cwd: join(import.meta.dirname, ".."), | |
| 38 | stdio: ["ignore", "ignore", "inherit"], | |
| 39 | }); | |
| 40 | } finally { | |
| 41 | rmSync(sqlFile, { force: true }); | |
| 42 | } | |
| 43 | ||
| 44 | const directory = join(import.meta.dirname, "../../../.credentials"); | |
| 45 | mkdirSync(directory, { recursive: true }); | |
| 46 | const file = join(directory, `${username}.txt`); | |
| 47 | writeFileSync(file, `username: ${username}\npassword: ${password}\n`); | |
| 48 | console.log(`Password for ${username} written to ${file}`); |