1
0

create-api-key.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { Resource } from "@opencode-ai/console-resource"
  2. import { and, Database, eq, isNull } from "../src/drizzle/index.js"
  3. import { Identifier } from "../src/identifier.js"
  4. import { AccountTable } from "../src/schema/account.sql.js"
  5. import { AuthTable } from "../src/schema/auth.sql.js"
  6. import { BillingTable } from "../src/schema/billing.sql.js"
  7. import { KeyTable } from "../src/schema/key.sql.js"
  8. import { UserTable } from "../src/schema/user.sql.js"
  9. import { WorkspaceTable } from "../src/schema/workspace.sql.js"
  10. import { centsToMicroCents } from "../src/util/price.js"
  11. const args = parseArgs(process.argv.slice(2))
  12. if (!args.email) {
  13. console.error(
  14. "Usage: bun script/create-api-key.ts --email <email> [--workspace-id <wrk_...>] [--workspace-name <name>] [--key-name <name>] [--balance-dollars <amount>] [--allow-production]",
  15. )
  16. process.exit(1)
  17. }
  18. if (Resource.App.stage === "production" && !args.allowProduction) {
  19. throw new Error("Refusing to create a production API key without --allow-production")
  20. }
  21. const result = await Database.transaction(async (tx) => {
  22. const auth = await tx
  23. .select()
  24. .from(AuthTable)
  25. .where(and(eq(AuthTable.provider, "email"), eq(AuthTable.subject, args.email)))
  26. .then((rows) => rows[0])
  27. const accountID = auth?.accountID ?? Identifier.create("account")
  28. if (!auth) {
  29. await tx.insert(AccountTable).values({ id: accountID })
  30. await tx.insert(AuthTable).values({
  31. id: Identifier.create("auth"),
  32. provider: "email",
  33. subject: args.email,
  34. accountID,
  35. })
  36. }
  37. const workspace = args.workspaceID
  38. ? await tx
  39. .select()
  40. .from(WorkspaceTable)
  41. .where(eq(WorkspaceTable.id, args.workspaceID))
  42. .then((rows) => rows[0])
  43. : await tx
  44. .select({ workspace: WorkspaceTable })
  45. .from(UserTable)
  46. .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
  47. .where(and(eq(UserTable.accountID, accountID), isNull(UserTable.timeDeleted)))
  48. .then((rows) => rows[0]?.workspace)
  49. if (args.workspaceID && !workspace) throw new Error(`Workspace not found: ${args.workspaceID}`)
  50. const workspaceID = workspace?.id ?? Identifier.create("workspace")
  51. if (!workspace) {
  52. await tx.insert(WorkspaceTable).values({
  53. id: workspaceID,
  54. slug: null,
  55. name: args.workspaceName ?? `${args.email} manual`,
  56. })
  57. }
  58. const user = await tx
  59. .select()
  60. .from(UserTable)
  61. .where(
  62. and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.accountID, accountID), isNull(UserTable.timeDeleted)),
  63. )
  64. .then((rows) => rows[0])
  65. const userID = user?.id ?? Identifier.create("user")
  66. if (!user) {
  67. await tx.insert(UserTable).values({
  68. id: userID,
  69. workspaceID,
  70. accountID,
  71. email: args.email,
  72. name: args.email,
  73. role: "admin",
  74. })
  75. }
  76. const balance = centsToMicroCents(args.balanceDollars * 100)
  77. const billing = await tx
  78. .select()
  79. .from(BillingTable)
  80. .where(eq(BillingTable.workspaceID, workspaceID))
  81. .then((rows) => rows[0])
  82. if (!billing) {
  83. await tx.insert(BillingTable).values({
  84. id: Identifier.create("billing"),
  85. workspaceID,
  86. balance,
  87. })
  88. } else if (billing.balance < balance) {
  89. await tx.update(BillingTable).set({ balance }).where(eq(BillingTable.workspaceID, workspaceID))
  90. }
  91. const secretKey = createSecretKey()
  92. const keyID = Identifier.create("key")
  93. await tx.insert(KeyTable).values({
  94. id: keyID,
  95. workspaceID,
  96. userID,
  97. name: args.keyName ?? "Manual API Key",
  98. key: secretKey,
  99. timeUsed: null,
  100. })
  101. return { accountID, workspaceID, userID, keyID, secretKey }
  102. })
  103. console.log(JSON.stringify({ stage: Resource.App.stage, ...result }, null, 2))
  104. function createSecretKey() {
  105. const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
  106. const values = new Uint32Array(64)
  107. crypto.getRandomValues(values)
  108. return `sk-${Array.from(values, (value) => chars[value % chars.length]).join("")}`
  109. }
  110. function parseArgs(argv: string[]) {
  111. const parsed = {
  112. email: "",
  113. workspaceID: "",
  114. workspaceName: "",
  115. keyName: "",
  116. balanceDollars: 100,
  117. allowProduction: false,
  118. }
  119. for (let index = 0; index < argv.length; index++) {
  120. const arg = argv[index]
  121. if (arg === "--email") parsed.email = requiredValue(argv, ++index, arg)
  122. if (arg === "--workspace-id") parsed.workspaceID = requiredValue(argv, ++index, arg)
  123. if (arg === "--workspace-name") parsed.workspaceName = requiredValue(argv, ++index, arg)
  124. if (arg === "--key-name") parsed.keyName = requiredValue(argv, ++index, arg)
  125. if (arg === "--balance-dollars") parsed.balanceDollars = Number(requiredValue(argv, ++index, arg))
  126. if (arg === "--allow-production") parsed.allowProduction = true
  127. }
  128. if (!Number.isFinite(parsed.balanceDollars) || parsed.balanceDollars < 0) throw new Error("Invalid --balance-dollars")
  129. return parsed
  130. }
  131. function requiredValue(argv: string[], index: number, arg: string) {
  132. const value = argv[index]
  133. if (!value || value.startsWith("--")) throw new Error(`Missing value for ${arg}`)
  134. return value
  135. }