@rebasepro/cli 0.6.1 → 0.7.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.
Files changed (49) hide show
  1. package/README.md +0 -1
  2. package/dist/commands/api-keys.d.ts +1 -0
  3. package/dist/commands/generate_sdk.d.ts +1 -1
  4. package/dist/index.es.js +399 -57
  5. package/dist/index.es.js.map +1 -1
  6. package/package.json +20 -15
  7. package/templates/template/backend/package.json +0 -1
  8. package/templates/template/backend/src/index.ts +1 -1
  9. package/templates/template/config/collections/posts.ts +1 -3
  10. package/templates/template/config/collections/tags.ts +1 -3
  11. package/templates/template/config/collections/users.ts +1 -4
  12. package/templates/template/package.json +0 -1
  13. package/templates/template/scripts/example.ts +4 -1
  14. package/skills/rebase-admin/SKILL.md +0 -710
  15. package/skills/rebase-api/SKILL.md +0 -662
  16. package/skills/rebase-api/references/.gitkeep +0 -3
  17. package/skills/rebase-auth/SKILL.md +0 -1143
  18. package/skills/rebase-auth/references/.gitkeep +0 -3
  19. package/skills/rebase-backend-postgres/SKILL.md +0 -633
  20. package/skills/rebase-backend-postgres/references/.gitkeep +0 -3
  21. package/skills/rebase-basics/SKILL.md +0 -749
  22. package/skills/rebase-basics/references/.gitkeep +0 -3
  23. package/skills/rebase-collections/SKILL.md +0 -1328
  24. package/skills/rebase-collections/references/.gitkeep +0 -3
  25. package/skills/rebase-cron-jobs/SKILL.md +0 -699
  26. package/skills/rebase-cron-jobs/references/.gitkeep +0 -1
  27. package/skills/rebase-custom-functions/SKILL.md +0 -233
  28. package/skills/rebase-deployment/SKILL.md +0 -885
  29. package/skills/rebase-deployment/references/.gitkeep +0 -3
  30. package/skills/rebase-design-language/SKILL.md +0 -692
  31. package/skills/rebase-email/SKILL.md +0 -701
  32. package/skills/rebase-email/references/.gitkeep +0 -1
  33. package/skills/rebase-entity-history/SKILL.md +0 -485
  34. package/skills/rebase-entity-history/references/.gitkeep +0 -1
  35. package/skills/rebase-local-env-setup/SKILL.md +0 -189
  36. package/skills/rebase-local-env-setup/references/.gitkeep +0 -3
  37. package/skills/rebase-realtime/SKILL.md +0 -755
  38. package/skills/rebase-realtime/references/.gitkeep +0 -3
  39. package/skills/rebase-sdk/SKILL.md +0 -594
  40. package/skills/rebase-sdk/references/.gitkeep +0 -0
  41. package/skills/rebase-storage/SKILL.md +0 -765
  42. package/skills/rebase-storage/references/.gitkeep +0 -3
  43. package/skills/rebase-studio/SKILL.md +0 -746
  44. package/skills/rebase-studio/references/.gitkeep +0 -3
  45. package/skills/rebase-ui-components/SKILL.md +0 -1488
  46. package/skills/rebase-ui-components/references/.gitkeep +0 -3
  47. package/skills/rebase-webhooks/SKILL.md +0 -623
  48. package/skills/rebase-webhooks/references/.gitkeep +0 -1
  49. package/templates/template/backend/drizzle.config.ts +0 -51
package/README.md CHANGED
@@ -23,7 +23,6 @@ The CLI is also bundled with every Rebase project as a local dependency.
23
23
  | `rebase db push` | Apply schema directly to database (dev) |
24
24
  | `rebase db generate` | Generate SQL migration files |
25
25
  | `rebase db migrate` | Run pending migrations |
26
- | `rebase db studio` | Open Drizzle Studio |
27
26
  | `rebase generate-sdk` | Generate a typed TypeScript SDK from collections |
28
27
  | `rebase auth reset-password` | Reset a user's password |
29
28
  | `rebase doctor` | Detect schema drift between collections, Drizzle schema, and database |
@@ -0,0 +1 @@
1
+ export declare function apiKeysCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void>;
@@ -2,7 +2,7 @@
2
2
  * CLI command: generate-sdk
3
3
  *
4
4
  * Reads collection definitions from a specified directory (default: ./config/collections),
5
- * generates a typed JS SDK, and writes it to the output directory (default: ./generated/sdk).
5
+ * generates a typed TypeScript SDK, and writes it to the output directory (default: ./generated/sdk).
6
6
  *
7
7
  * Uses jiti for dynamic TypeScript import of collection files.
8
8
  */
package/dist/index.es.js CHANGED
@@ -11,6 +11,7 @@ import { fileURLToPath } from "url";
11
11
  import crypto from "crypto";
12
12
  import { generateSDK } from "@rebasepro/sdk-generator";
13
13
  import { execSync, spawn } from "child_process";
14
+ import { createRequire } from "module";
14
15
  //#region src/utils/package-manager.ts
15
16
  /**
16
17
  * Package manager detection and command abstraction.
@@ -130,9 +131,9 @@ function getPMCommands(pm) {
130
131
  //#endregion
131
132
  //#region src/commands/init.ts
132
133
  var access = promisify(fs.access);
133
- var __filename$2 = fileURLToPath(import.meta.url);
134
- var __dirname$2 = path.dirname(__filename$2);
135
- function findParentDir$1(currentDir, targetName) {
134
+ var __filename$1 = fileURLToPath(import.meta.url);
135
+ var __dirname$1 = path.dirname(__filename$1);
136
+ function findParentDir(currentDir, targetName) {
136
137
  const root = path.parse(currentDir).root;
137
138
  while (currentDir && currentDir !== root) {
138
139
  if (path.basename(currentDir) === targetName) return currentDir;
@@ -140,7 +141,7 @@ function findParentDir$1(currentDir, targetName) {
140
141
  }
141
142
  return null;
142
143
  }
143
- var cliRoot = findParentDir$1(__dirname$2, "cli");
144
+ var cliRoot = findParentDir(__dirname$1, "cli");
144
145
  var PRESET_CHOICES = [
145
146
  {
146
147
  name: "Blog — Posts, Authors, Tags (with markdown editor)",
@@ -574,7 +575,7 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
574
575
  * CLI command: generate-sdk
575
576
  *
576
577
  * Reads collection definitions from a specified directory (default: ./config/collections),
577
- * generates a typed JS SDK, and writes it to the output directory (default: ./generated/sdk).
578
+ * generates a typed TypeScript SDK, and writes it to the output directory (default: ./generated/sdk).
578
579
  *
579
580
  * Uses jiti for dynamic TypeScript import of collection files.
580
581
  */
@@ -720,7 +721,7 @@ function findProjectRoot(startDir = process.cwd()) {
720
721
  try {
721
722
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
722
723
  if (pkg.workspaces && Array.isArray(pkg.workspaces)) {
723
- if (pkg.workspaces.some((w) => w === "backend" || w.includes("backend"))) return dir;
724
+ if (pkg.workspaces.some((w) => w === "backend")) return dir;
724
725
  }
725
726
  } catch {}
726
727
  if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "config"))) return dir;
@@ -962,7 +963,6 @@ ${chalk.green.bold("Commands")}
962
963
  ${chalk.blue.bold("push")} Apply schema directly to database (development)
963
964
  ${chalk.blue.bold("generate")} Generate migration files
964
965
  ${chalk.blue.bold("migrate")} Run pending migrations
965
- ${chalk.blue.bold("studio")} Open Studio viewer
966
966
  ${chalk.blue.bold("branch")} Database branching (create, list, delete, info)
967
967
 
968
968
  ${chalk.green.bold("Examples")}
@@ -1431,19 +1431,90 @@ async function resetPassword(rawArgs) {
1431
1431
  process.exit(1);
1432
1432
  }
1433
1433
  const projectRoot = requireProjectRoot();
1434
+ let envServiceKey;
1435
+ const envFile = findEnvFile(projectRoot);
1436
+ if (envFile && fs.existsSync(envFile)) try {
1437
+ const match = fs.readFileSync(envFile, "utf8").match(/^\s*REBASE_SERVICE_KEY\s*=\s*['"]?(.*?)['"]?\s*$/m);
1438
+ if (match && match[1]) envServiceKey = match[1];
1439
+ } catch {}
1440
+ let baseUrl = process.env.REBASE_BASE_URL;
1441
+ let serviceKey = process.env.REBASE_SERVICE_KEY || envServiceKey;
1442
+ const statePath = path.join(projectRoot, ".rebase", "state.json");
1443
+ if (fs.existsSync(statePath)) try {
1444
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
1445
+ if (state && typeof state === "object") {
1446
+ if (typeof state.baseUrl === "string" && !baseUrl) baseUrl = state.baseUrl;
1447
+ if (typeof state.serviceKey === "string" && !serviceKey) serviceKey = state.serviceKey;
1448
+ }
1449
+ } catch {}
1450
+ const devUrlPath = path.join(projectRoot, ".rebase-dev-url");
1451
+ if (fs.existsSync(devUrlPath) && !baseUrl) try {
1452
+ baseUrl = fs.readFileSync(devUrlPath, "utf8").trim();
1453
+ } catch {}
1454
+ if (baseUrl && serviceKey) {
1455
+ console.log("Trying API-first reset via running backend...");
1456
+ try {
1457
+ const finalPass = newPassword || "NewPassword123!";
1458
+ const cleanBaseUrl = baseUrl.replace(/\/+$/, "");
1459
+ const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=1`;
1460
+ const searchRes = await fetch(searchUrl, { headers: {
1461
+ "Authorization": `Bearer ${serviceKey}`,
1462
+ "Accept": "application/json"
1463
+ } });
1464
+ if (!searchRes.ok) throw new Error(`Failed to list users: ${searchRes.statusText}`);
1465
+ const searchData = await searchRes.json();
1466
+ if (!searchData || typeof searchData !== "object") throw new Error("Invalid response format from user search API.");
1467
+ let userId;
1468
+ if (Array.isArray(searchData)) {
1469
+ const firstUser = searchData[0];
1470
+ if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
1471
+ else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
1472
+ } else if ("users" in searchData && Array.isArray(searchData.users)) {
1473
+ const firstUser = searchData.users[0];
1474
+ if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
1475
+ else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
1476
+ }
1477
+ if (!userId) throw new Error(`User not found with email: ${email}`);
1478
+ const resetUrl = `${cleanBaseUrl}/api/admin/users/${userId}/reset-password`;
1479
+ const resetRes = await fetch(resetUrl, {
1480
+ method: "POST",
1481
+ headers: {
1482
+ "Authorization": `Bearer ${serviceKey}`,
1483
+ "Content-Type": "application/json",
1484
+ "Accept": "application/json"
1485
+ },
1486
+ body: JSON.stringify({ password: finalPass })
1487
+ });
1488
+ if (!resetRes.ok) {
1489
+ const errText = await resetRes.text();
1490
+ throw new Error(`Password reset endpoint failed: ${errText || resetRes.statusText}`);
1491
+ }
1492
+ console.log("API reset successful.");
1493
+ console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (via API)"));
1494
+ console.log("");
1495
+ console.log(` ${chalk.gray("Email:")} ${email}`);
1496
+ console.log(` ${chalk.gray("Password:")} ${finalPass}`);
1497
+ console.log("");
1498
+ return;
1499
+ } catch (err) {
1500
+ const errMsg = err instanceof Error ? err.message : String(err);
1501
+ console.warn(chalk.yellow("API reset failed, falling back to direct database update..."));
1502
+ console.warn(chalk.gray(` Details: ${errMsg}`));
1503
+ }
1504
+ }
1434
1505
  const backendDir = requireBackendDir(projectRoot);
1435
1506
  const tsxBin = resolveTsx(projectRoot);
1436
1507
  if (!tsxBin) {
1437
1508
  console.error(chalk.red("✗ Could not find tsx binary."));
1438
1509
  process.exit(1);
1439
1510
  }
1440
- const envFile = findEnvFile(projectRoot);
1441
- const env = { ...process.env };
1442
- if (envFile) env.DOTENV_CONFIG_PATH = envFile;
1443
- env.REBASE_RESET_EMAIL = email;
1444
- env.REBASE_RESET_PASSWORD = newPassword || "NewPassword123!";
1445
- env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, ".env");
1446
- const scriptContent = `
1511
+ try {
1512
+ const env = { ...process.env };
1513
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
1514
+ env.REBASE_RESET_EMAIL = email;
1515
+ env.REBASE_RESET_PASSWORD = newPassword || "NewPassword123!";
1516
+ env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, ".env");
1517
+ const scriptContent = `
1447
1518
  import { createPostgresDatabaseConnection } from "@rebasepro/server-postgresql";
1448
1519
  import { hashPassword } from "@rebasepro/server-core";
1449
1520
  import { eq } from "drizzle-orm";
@@ -1497,28 +1568,33 @@ async function resetPassword() {
1497
1568
 
1498
1569
  resetPassword().catch(console.error);
1499
1570
  `;
1500
- const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
1501
- fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
1502
- console.log("");
1503
- console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password"));
1504
- console.log("");
1505
- console.log(` ${chalk.gray("Email:")} ${email}`);
1506
- if (newPassword) console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
1507
- console.log("");
1508
- const child = spawn(tsxBin, [tmpScriptPath], {
1509
- cwd: backendDir,
1510
- stdio: "inherit",
1511
- env
1512
- });
1513
- return new Promise((resolve) => {
1514
- child.on("close", (code) => {
1515
- try {
1516
- fs.unlinkSync(tmpScriptPath);
1517
- } catch {}
1518
- if (code !== 0) process.exit(code ?? 1);
1519
- resolve();
1571
+ const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
1572
+ fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
1573
+ console.log("");
1574
+ console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (Direct DB Fallback)"));
1575
+ console.log("");
1576
+ console.log(` ${chalk.gray("Email:")} ${email}`);
1577
+ if (newPassword) console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
1578
+ console.log("");
1579
+ const child = spawn(tsxBin, [tmpScriptPath], {
1580
+ cwd: backendDir,
1581
+ stdio: "inherit",
1582
+ env
1520
1583
  });
1521
- });
1584
+ return new Promise((resolve) => {
1585
+ child.on("close", (code) => {
1586
+ try {
1587
+ fs.unlinkSync(tmpScriptPath);
1588
+ } catch {}
1589
+ if (code !== 0) process.exit(code ?? 1);
1590
+ resolve();
1591
+ });
1592
+ });
1593
+ } catch (err) {
1594
+ console.error(chalk.red("✗ Direct database update failed."));
1595
+ console.error(err instanceof Error ? err.message : String(err));
1596
+ process.exit(1);
1597
+ }
1522
1598
  }
1523
1599
  function printAuthHelp() {
1524
1600
  console.log(`
@@ -1587,8 +1663,7 @@ async function doctorCommand(rawArgs) {
1587
1663
  }
1588
1664
  //#endregion
1589
1665
  //#region src/commands/skills.ts
1590
- var __filename$1 = fileURLToPath(import.meta.url);
1591
- var __dirname$1 = path.dirname(__filename$1);
1666
+ var require = createRequire(import.meta.url);
1592
1667
  /** Supported agent environments and their target directories. */
1593
1668
  var AGENTS = {
1594
1669
  cursor: {
@@ -1632,24 +1707,16 @@ var AGENTS = {
1632
1707
  })
1633
1708
  }
1634
1709
  };
1635
- function findParentDir(currentDir, targetName) {
1636
- const root = path.parse(currentDir).root;
1637
- while (currentDir && currentDir !== root) {
1638
- if (path.basename(currentDir) === targetName) return currentDir;
1639
- currentDir = path.dirname(currentDir);
1640
- }
1641
- return null;
1642
- }
1643
- /** Resolve the path to the bundled skills directory. */
1710
+ /**
1711
+ * Resolve the path to the skills directory from @rebasepro/agent-skills.
1712
+ * Works in both workspace (symlink) and published (real files) layouts.
1713
+ */
1644
1714
  function getSkillsSourceDir() {
1645
- const cliRoot = findParentDir(__dirname$1, "cli");
1646
- if (cliRoot) {
1647
- const dir = path.join(cliRoot, "skills");
1648
- if (fs.existsSync(dir)) return dir;
1649
- }
1650
- const distSkills = path.resolve(__dirname$1, "../../skills");
1651
- if (fs.existsSync(distSkills)) return distSkills;
1652
- throw new Error("Could not find bundled skills directory. Make sure the CLI was built with `pnpm build`.");
1715
+ const pkgJsonPath = require.resolve("@rebasepro/agent-skills/package.json");
1716
+ const pkgRoot = path.dirname(pkgJsonPath);
1717
+ const skillsDir = path.join(pkgRoot, "skills");
1718
+ if (!fs.existsSync(skillsDir)) throw new Error(`Skills directory not found at ${skillsDir}. Make sure @rebasepro/agent-skills is installed.`);
1719
+ return skillsDir;
1653
1720
  }
1654
1721
  /** Read all skill directories and return their names + content. */
1655
1722
  function loadSkills(skillsDir) {
@@ -1764,6 +1831,272 @@ ${chalk.green.bold("Examples")}
1764
1831
  `);
1765
1832
  }
1766
1833
  //#endregion
1834
+ //#region src/commands/api-keys.ts
1835
+ /**
1836
+ * CLI command: rebase api-keys <action>
1837
+ *
1838
+ * Subcommands:
1839
+ * list — List all API keys (masked)
1840
+ * create — Create a new API key
1841
+ * revoke — Revoke an existing API key
1842
+ */
1843
+ function loadEnv(projectRoot) {
1844
+ const envFile = findEnvFile(projectRoot);
1845
+ const env = {};
1846
+ if (envFile && fs.existsSync(envFile)) {
1847
+ const content = fs.readFileSync(envFile, "utf-8");
1848
+ for (const line of content.split("\n")) {
1849
+ const trimmed = line.trim();
1850
+ if (!trimmed || trimmed.startsWith("#")) continue;
1851
+ const idx = trimmed.indexOf("=");
1852
+ if (idx > 0) {
1853
+ const key = trimmed.slice(0, idx).trim();
1854
+ let value = trimmed.slice(idx + 1).trim();
1855
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1856
+ env[key] = value;
1857
+ }
1858
+ }
1859
+ }
1860
+ return env;
1861
+ }
1862
+ function resolveBaseUrl(env) {
1863
+ const port = env.PORT || env.REBASE_PORT || "3001";
1864
+ return env.REBASE_BASE_URL || `http://localhost:${port}`;
1865
+ }
1866
+ async function apiKeysCommand(subcommand, rawArgs) {
1867
+ if (!subcommand || subcommand === "--help") {
1868
+ printApiKeysHelp();
1869
+ return;
1870
+ }
1871
+ switch (subcommand) {
1872
+ case "list":
1873
+ await listKeys(rawArgs);
1874
+ break;
1875
+ case "create":
1876
+ await createKey(rawArgs);
1877
+ break;
1878
+ case "revoke":
1879
+ await revokeKey(rawArgs);
1880
+ break;
1881
+ default:
1882
+ console.error(chalk.red(`Unknown api-keys command: ${subcommand}`));
1883
+ console.log("");
1884
+ printApiKeysHelp();
1885
+ process.exit(1);
1886
+ }
1887
+ }
1888
+ async function listKeys(_rawArgs) {
1889
+ const env = loadEnv(requireProjectRoot());
1890
+ const baseUrl = resolveBaseUrl(env);
1891
+ const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
1892
+ if (!serviceKey) {
1893
+ console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
1894
+ process.exit(1);
1895
+ }
1896
+ try {
1897
+ const res = await fetch(`${baseUrl}/api/admin/api-keys`, { headers: { Authorization: `Bearer ${serviceKey}` } });
1898
+ if (!res.ok) {
1899
+ const body = await res.text();
1900
+ console.error(chalk.red(`✗ Failed to list API keys: ${res.status} ${body}`));
1901
+ process.exit(1);
1902
+ }
1903
+ const { keys } = await res.json();
1904
+ console.log("");
1905
+ console.log(chalk.bold(" 🔑 API Keys"));
1906
+ console.log("");
1907
+ if (keys.length === 0) {
1908
+ console.log(chalk.gray(" No API keys found."));
1909
+ console.log("");
1910
+ return;
1911
+ }
1912
+ for (const key of keys) {
1913
+ const status = key.revoked_at ? chalk.red("revoked") : key.expires_at && new Date(key.expires_at) < /* @__PURE__ */ new Date() ? chalk.yellow("expired") : chalk.green("active");
1914
+ const perms = key.permissions.map((p) => `${p.collection}(${p.operations.join(",")})`).join(", ");
1915
+ console.log(` ${chalk.bold(key.name)} ${chalk.gray(`[${key.key_prefix}•••]`)} ${status}`);
1916
+ console.log(` ${chalk.gray("ID:")} ${key.id}`);
1917
+ console.log(` ${chalk.gray("Permissions:")} ${perms || "none"}`);
1918
+ console.log(` ${chalk.gray("Created:")} ${new Date(key.created_at).toLocaleDateString()}`);
1919
+ if (key.last_used_at) console.log(` ${chalk.gray("Last used:")} ${new Date(key.last_used_at).toLocaleDateString()}`);
1920
+ console.log("");
1921
+ }
1922
+ } catch (e) {
1923
+ console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
1924
+ console.error(chalk.gray(" Is the Rebase server running?"));
1925
+ process.exit(1);
1926
+ }
1927
+ }
1928
+ async function createKey(rawArgs) {
1929
+ const args = arg({
1930
+ "--name": String,
1931
+ "--permissions": String,
1932
+ "--admin": Boolean,
1933
+ "--rate-limit": Number,
1934
+ "--expires": String,
1935
+ "-n": "--name"
1936
+ }, {
1937
+ argv: rawArgs.slice(4),
1938
+ permissive: true
1939
+ });
1940
+ const name = args["--name"] || args._[0];
1941
+ const permissionsRaw = args["--permissions"];
1942
+ if (!name) {
1943
+ console.error(chalk.red("✗ Name is required."));
1944
+ console.log("");
1945
+ console.log(chalk.gray(" Usage: rebase api-keys create --name \"My Key\" --permissions '[{\"collection\":\"*\",\"operations\":[\"read\"]}]'"));
1946
+ process.exit(1);
1947
+ }
1948
+ let permissions;
1949
+ if (permissionsRaw) try {
1950
+ permissions = JSON.parse(permissionsRaw);
1951
+ } catch {
1952
+ console.error(chalk.red("✗ Invalid --permissions JSON."));
1953
+ console.log(chalk.gray(" Example: '[{\"collection\":\"*\",\"operations\":[\"read\",\"write\"]}]'"));
1954
+ process.exit(1);
1955
+ return;
1956
+ }
1957
+ else permissions = [{
1958
+ collection: "*",
1959
+ operations: [
1960
+ "read",
1961
+ "write",
1962
+ "delete"
1963
+ ]
1964
+ }];
1965
+ let expires_at = null;
1966
+ const expiresFlag = args["--expires"];
1967
+ if (expiresFlag) {
1968
+ const days = {
1969
+ "7d": 7,
1970
+ "30d": 30,
1971
+ "90d": 90,
1972
+ "1y": 365
1973
+ };
1974
+ if (days[expiresFlag]) expires_at = new Date(Date.now() + days[expiresFlag] * 864e5).toISOString();
1975
+ else {
1976
+ const parsed = new Date(expiresFlag);
1977
+ if (isNaN(parsed.getTime())) {
1978
+ console.error(chalk.red("✗ Invalid --expires value. Use 7d, 30d, 90d, 1y, or an ISO date."));
1979
+ process.exit(1);
1980
+ }
1981
+ expires_at = parsed.toISOString();
1982
+ }
1983
+ }
1984
+ const env = loadEnv(requireProjectRoot());
1985
+ const baseUrl = resolveBaseUrl(env);
1986
+ const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
1987
+ if (!serviceKey) {
1988
+ console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
1989
+ process.exit(1);
1990
+ }
1991
+ try {
1992
+ const body = {
1993
+ name,
1994
+ permissions,
1995
+ admin: args["--admin"] ?? false,
1996
+ rate_limit: args["--rate-limit"] ?? null,
1997
+ expires_at
1998
+ };
1999
+ const res = await fetch(`${baseUrl}/api/admin/api-keys`, {
2000
+ method: "POST",
2001
+ headers: {
2002
+ Authorization: `Bearer ${serviceKey}`,
2003
+ "Content-Type": "application/json"
2004
+ },
2005
+ body: JSON.stringify(body)
2006
+ });
2007
+ if (!res.ok) {
2008
+ const errBody = await res.text();
2009
+ console.error(chalk.red(`✗ Failed to create API key: ${res.status} ${errBody}`));
2010
+ process.exit(1);
2011
+ }
2012
+ const { key } = await res.json();
2013
+ console.log("");
2014
+ console.log(chalk.bold.green(" ✓ API key created successfully"));
2015
+ console.log("");
2016
+ console.log(` ${chalk.gray("Name:")} ${key.name}`);
2017
+ console.log(` ${chalk.gray("ID:")} ${key.id}`);
2018
+ console.log(` ${chalk.gray("Prefix:")} ${key.key_prefix}`);
2019
+ console.log("");
2020
+ console.log(chalk.bold.yellow(" ⚠ Copy your key now — it won't be shown again:"));
2021
+ console.log("");
2022
+ console.log(` ${chalk.cyan(key.key)}`);
2023
+ console.log("");
2024
+ } catch (e) {
2025
+ console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
2026
+ console.error(chalk.gray(" Is the Rebase server running?"));
2027
+ process.exit(1);
2028
+ }
2029
+ }
2030
+ async function revokeKey(rawArgs) {
2031
+ const args = arg({ "--id": String }, {
2032
+ argv: rawArgs.slice(4),
2033
+ permissive: true
2034
+ });
2035
+ const id = args["--id"] || args._[0];
2036
+ if (!id) {
2037
+ console.error(chalk.red("✗ Key ID is required."));
2038
+ console.log("");
2039
+ console.log(chalk.gray(" Usage: rebase api-keys revoke <key-id>"));
2040
+ process.exit(1);
2041
+ }
2042
+ const env = loadEnv(requireProjectRoot());
2043
+ const baseUrl = resolveBaseUrl(env);
2044
+ const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
2045
+ if (!serviceKey) {
2046
+ console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
2047
+ process.exit(1);
2048
+ }
2049
+ try {
2050
+ const res = await fetch(`${baseUrl}/api/admin/api-keys/${encodeURIComponent(id)}`, {
2051
+ method: "DELETE",
2052
+ headers: { Authorization: `Bearer ${serviceKey}` }
2053
+ });
2054
+ if (!res.ok) {
2055
+ const errBody = await res.text();
2056
+ console.error(chalk.red(`✗ Failed to revoke API key: ${res.status} ${errBody}`));
2057
+ process.exit(1);
2058
+ }
2059
+ console.log("");
2060
+ console.log(chalk.bold.green(" ✓ API key revoked successfully"));
2061
+ console.log(` ${chalk.gray("ID:")} ${id}`);
2062
+ console.log("");
2063
+ } catch (e) {
2064
+ console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
2065
+ console.error(chalk.gray(" Is the Rebase server running?"));
2066
+ process.exit(1);
2067
+ }
2068
+ }
2069
+ function printApiKeysHelp() {
2070
+ console.log(`
2071
+ ${chalk.bold("rebase api-keys")} — Manage Service API Keys
2072
+
2073
+ ${chalk.green.bold("Usage")}
2074
+ rebase api-keys ${chalk.blue("<command>")} [options]
2075
+
2076
+ ${chalk.green.bold("Commands")}
2077
+ ${chalk.blue.bold("list")} List all API keys
2078
+ ${chalk.blue.bold("create")} Create a new API key
2079
+ ${chalk.blue.bold("revoke")} Revoke an API key
2080
+
2081
+ ${chalk.green.bold("create Options")}
2082
+ ${chalk.blue("--name, -n")} Key name ${chalk.gray("(required)")}
2083
+ ${chalk.blue("--permissions")} JSON array of permissions
2084
+ ${chalk.gray("Default: [{\"collection\":\"*\",\"operations\":[\"read\",\"write\",\"delete\"]}]")}
2085
+ ${chalk.blue("--admin")} Grant admin role (access to admin routes)
2086
+ ${chalk.blue("--rate-limit")} Requests per 15-min window ${chalk.gray("(default: 1000)")}
2087
+ ${chalk.blue("--expires")} Expiration: 7d, 30d, 90d, 1y, or ISO date
2088
+
2089
+ ${chalk.green.bold("revoke Options")}
2090
+ ${chalk.blue("--id")} API key ID to revoke ${chalk.gray("(or positional arg)")}
2091
+
2092
+ ${chalk.green.bold("Examples")}
2093
+ rebase api-keys list
2094
+ rebase api-keys create --name "Analytics" --permissions '[{"collection":"events","operations":["read"]}]'
2095
+ rebase api-keys create -n "Full Access" --expires 90d
2096
+ rebase api-keys revoke abc123-def456
2097
+ `);
2098
+ }
2099
+ //#endregion
1767
2100
  //#region src/cli.ts
1768
2101
  var __filename = fileURLToPath(import.meta.url);
1769
2102
  var __dirname = path.dirname(__filename);
@@ -1798,7 +2131,8 @@ async function entry(args) {
1798
2131
  "start",
1799
2132
  "auth",
1800
2133
  "doctor",
1801
- "skills"
2134
+ "skills",
2135
+ "api-keys"
1802
2136
  ].includes(command)) {
1803
2137
  printHelp();
1804
2138
  return;
@@ -1849,6 +2183,9 @@ async function entry(args) {
1849
2183
  case "skills":
1850
2184
  await skillsCommand(effectiveSubcommand, args);
1851
2185
  break;
2186
+ case "api-keys":
2187
+ await apiKeysCommand(effectiveSubcommand, args);
2188
+ break;
1852
2189
  default:
1853
2190
  console.log(chalk.red(`Unknown command: ${command}`));
1854
2191
  console.log("");
@@ -1877,11 +2214,10 @@ ${chalk.green.bold("Database")}
1877
2214
  ${chalk.blue.bold("db push")} Apply schema directly to database ${chalk.gray("(dev)")}
1878
2215
  ${chalk.blue.bold("db generate")} Generate SQL migration files
1879
2216
  ${chalk.blue.bold("db migrate")} Run pending migrations
1880
- ${chalk.blue.bold("db studio")} Open Drizzle Studio
1881
2217
  ${chalk.blue.bold("db")} ${chalk.gray("--help")} Show database command help
1882
2218
 
1883
2219
  ${chalk.green.bold("SDK")}
1884
- ${chalk.blue.bold("generate-sdk")} Generate a typed JS SDK from collections
2220
+ ${chalk.blue.bold("generate-sdk")} Generate a typed TypeScript SDK from collections
1885
2221
 
1886
2222
  ${chalk.green.bold("Auth")}
1887
2223
  ${chalk.blue.bold("auth reset-password")} Reset a user's password
@@ -1893,6 +2229,12 @@ ${chalk.green.bold("Diagnostics")}
1893
2229
  ${chalk.green.bold("AI Agent Skills")}
1894
2230
  ${chalk.blue.bold("skills install")} Install Rebase agent skills for your AI coding assistant
1895
2231
 
2232
+ ${chalk.green.bold("API Keys")}
2233
+ ${chalk.blue.bold("api-keys list")} List all service API keys
2234
+ ${chalk.blue.bold("api-keys create")} Create a new scoped API key
2235
+ ${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
2236
+ ${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
2237
+
1896
2238
  ${chalk.green.bold("Options")}
1897
2239
  ${chalk.blue("--version, -v")} Show version number
1898
2240
  ${chalk.blue("--help, -h")} Show this help message