@auth-gate/rbac 0.8.1 → 0.9.1

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.
@@ -150,6 +150,21 @@ function validateRbacConfig(config) {
150
150
  }
151
151
  }
152
152
  }
153
+ const defaultRoles = [];
154
+ for (const key of roleKeys) {
155
+ const role = roles[key];
156
+ if (role.isDefault !== void 0 && typeof role.isDefault !== "boolean") {
157
+ throw new Error(`Role "${key}".isDefault must be a boolean.`);
158
+ }
159
+ if (role.isDefault === true) {
160
+ defaultRoles.push(key);
161
+ }
162
+ }
163
+ if (defaultRoles.length > 1) {
164
+ throw new Error(
165
+ `Only one role may have isDefault: true. Found ${defaultRoles.length}: ${defaultRoles.join(", ")}.`
166
+ );
167
+ }
153
168
  for (const key of roleKeys) {
154
169
  const role = roles[key];
155
170
  if (role.inherits !== void 0) {
@@ -258,7 +273,6 @@ var CONFIG_FILENAMES = [
258
273
  "authgate.rbac.mjs"
259
274
  ];
260
275
  async function loadRbacConfig(cwd) {
261
- var _a, _b;
262
276
  let configPath = null;
263
277
  for (const filename of CONFIG_FILENAMES) {
264
278
  const candidate = resolve(cwd, filename);
@@ -273,8 +287,12 @@ async function loadRbacConfig(cwd) {
273
287
  Run \`npx @auth-gate/rbac init\` to create one.`
274
288
  );
275
289
  }
290
+ return loadRbacConfigFromPath(configPath, cwd);
291
+ }
292
+ async function loadRbacConfigFromPath(configPath, cwd) {
293
+ var _a, _b;
276
294
  const { createJiti } = await import("jiti");
277
- const jiti = createJiti(cwd, { interopDefault: true });
295
+ const jiti = createJiti(cwd != null ? cwd : process.cwd(), { interopDefault: true });
278
296
  const mod = await jiti.import(configPath);
279
297
  const raw = (_b = (_a = mod.default) != null ? _a : mod.rbac) != null ? _b : mod;
280
298
  const config = raw && typeof raw === "object" && "_config" in raw ? raw._config : raw;
@@ -302,7 +320,7 @@ function hashConditionSource(fn) {
302
320
  return createHash("sha256").update(fn.toString()).digest("hex").slice(0, 16);
303
321
  }
304
322
  function computeRbacDiff(config, server, memberCounts) {
305
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
323
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
306
324
  const resourceOps = [];
307
325
  const roleOps = [];
308
326
  const conditionOps = [];
@@ -393,6 +411,11 @@ function computeRbacDiff(config, server, memberCounts) {
393
411
  if (existingInherits !== configInherits) {
394
412
  changes.push("inherits changed");
395
413
  }
414
+ const existingDefault = (_l = existing.isDefault) != null ? _l : false;
415
+ const configDefault = (_m = role.isDefault) != null ? _m : false;
416
+ if (existingDefault !== configDefault) {
417
+ changes.push(`isDefault: ${existingDefault} -> ${configDefault}`);
418
+ }
396
419
  if (changes.length > 0) {
397
420
  roleOps.push({ type: "update", key, role, existing, changes });
398
421
  }
@@ -402,7 +425,7 @@ function computeRbacDiff(config, server, memberCounts) {
402
425
  for (const [key, role] of serverRoleByKey) {
403
426
  if (renameMap.has(key)) continue;
404
427
  if (role.isActive) {
405
- const members = (_l = memberCounts[role.configKey]) != null ? _l : 0;
428
+ const members = (_n = memberCounts[role.configKey]) != null ? _n : 0;
406
429
  if (members > 0) hasDestructive = true;
407
430
  roleOps.push({ type: "archive", key, existing: role, assignedMembers: members });
408
431
  }
@@ -489,6 +512,9 @@ function formatRbacDiff(diff, dryRun) {
489
512
  chalk.dim(` inherits: [${[...op.role.inherits].join(", ")}]`)
490
513
  );
491
514
  }
515
+ if (op.role.isDefault) {
516
+ lines.push(chalk.dim(` isDefault: true`));
517
+ }
492
518
  } else if (op.type === "update") {
493
519
  lines.push(chalk.yellow(` ~ UPDATE role "${op.key}"`));
494
520
  for (const change of op.changes) {
@@ -626,6 +652,7 @@ export {
626
652
  __spreadValues,
627
653
  validateRbacConfig,
628
654
  loadRbacConfig,
655
+ loadRbacConfigFromPath,
629
656
  hashConditionSource,
630
657
  computeRbacDiff,
631
658
  formatRbacDiff,
package/dist/cli.cjs CHANGED
@@ -170,6 +170,21 @@ function validateRbacConfig(config) {
170
170
  }
171
171
  }
172
172
  }
173
+ const defaultRoles = [];
174
+ for (const key of roleKeys) {
175
+ const role = roles[key];
176
+ if (role.isDefault !== void 0 && typeof role.isDefault !== "boolean") {
177
+ throw new Error(`Role "${key}".isDefault must be a boolean.`);
178
+ }
179
+ if (role.isDefault === true) {
180
+ defaultRoles.push(key);
181
+ }
182
+ }
183
+ if (defaultRoles.length > 1) {
184
+ throw new Error(
185
+ `Only one role may have isDefault: true. Found ${defaultRoles.length}: ${defaultRoles.join(", ")}.`
186
+ );
187
+ }
173
188
  for (const key of roleKeys) {
174
189
  const role = roles[key];
175
190
  if (role.inherits !== void 0) {
@@ -276,7 +291,6 @@ var CONFIG_FILENAMES = [
276
291
  "authgate.rbac.mjs"
277
292
  ];
278
293
  async function loadRbacConfig(cwd) {
279
- var _a, _b;
280
294
  let configPath = null;
281
295
  for (const filename of CONFIG_FILENAMES) {
282
296
  const candidate = (0, import_path.resolve)(cwd, filename);
@@ -291,8 +305,12 @@ async function loadRbacConfig(cwd) {
291
305
  Run \`npx @auth-gate/rbac init\` to create one.`
292
306
  );
293
307
  }
308
+ return loadRbacConfigFromPath(configPath, cwd);
309
+ }
310
+ async function loadRbacConfigFromPath(configPath, cwd) {
311
+ var _a, _b;
294
312
  const { createJiti } = await import("jiti");
295
- const jiti = createJiti(cwd, { interopDefault: true });
313
+ const jiti = createJiti(cwd != null ? cwd : process.cwd(), { interopDefault: true });
296
314
  const mod = await jiti.import(configPath);
297
315
  const raw = (_b = (_a = mod.default) != null ? _a : mod.rbac) != null ? _b : mod;
298
316
  const config = raw && typeof raw === "object" && "_config" in raw ? raw._config : raw;
@@ -320,7 +338,7 @@ function hashConditionSource(fn) {
320
338
  return createHash("sha256").update(fn.toString()).digest("hex").slice(0, 16);
321
339
  }
322
340
  function computeRbacDiff(config, server, memberCounts) {
323
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
341
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
324
342
  const resourceOps = [];
325
343
  const roleOps = [];
326
344
  const conditionOps = [];
@@ -411,6 +429,11 @@ function computeRbacDiff(config, server, memberCounts) {
411
429
  if (existingInherits !== configInherits) {
412
430
  changes.push("inherits changed");
413
431
  }
432
+ const existingDefault = (_l = existing.isDefault) != null ? _l : false;
433
+ const configDefault = (_m = role.isDefault) != null ? _m : false;
434
+ if (existingDefault !== configDefault) {
435
+ changes.push(`isDefault: ${existingDefault} -> ${configDefault}`);
436
+ }
414
437
  if (changes.length > 0) {
415
438
  roleOps.push({ type: "update", key, role, existing, changes });
416
439
  }
@@ -420,7 +443,7 @@ function computeRbacDiff(config, server, memberCounts) {
420
443
  for (const [key, role] of serverRoleByKey) {
421
444
  if (renameMap.has(key)) continue;
422
445
  if (role.isActive) {
423
- const members = (_l = memberCounts[role.configKey]) != null ? _l : 0;
446
+ const members = (_n = memberCounts[role.configKey]) != null ? _n : 0;
424
447
  if (members > 0) hasDestructive = true;
425
448
  roleOps.push({ type: "archive", key, existing: role, assignedMembers: members });
426
449
  }
@@ -507,6 +530,9 @@ function formatRbacDiff(diff, dryRun) {
507
530
  import_chalk.default.dim(` inherits: [${[...op.role.inherits].join(", ")}]`)
508
531
  );
509
532
  }
533
+ if (op.role.isDefault) {
534
+ lines.push(import_chalk.default.dim(` isDefault: true`));
535
+ }
510
536
  } else if (op.type === "update") {
511
537
  lines.push(import_chalk.default.yellow(` ~ UPDATE role "${op.key}"`));
512
538
  for (const change of op.changes) {
@@ -640,10 +666,76 @@ var RbacSyncClient = class {
640
666
  }
641
667
  };
642
668
 
669
+ // src/project-config-loader.ts
670
+ var import_path2 = require("path");
671
+ var import_fs2 = require("fs");
672
+ var CONFIG_FILENAMES2 = [
673
+ "authgate.config.ts",
674
+ "authgate.config.js",
675
+ "authgate.config.mjs"
676
+ ];
677
+ var AUTO_ENV_FILES = [".env", ".env.local"];
678
+ async function resolveProjectConfig(cwd) {
679
+ var _a, _b, _c;
680
+ await autoLoadEnvFiles(cwd);
681
+ const config = await loadProjectConfigFile(cwd);
682
+ if (!config) {
683
+ return {
684
+ connection: {
685
+ baseUrl: process.env.AUTHGATE_BASE_URL,
686
+ apiKey: process.env.AUTHGATE_API_KEY
687
+ },
688
+ rbacConfigPath: null,
689
+ billingConfigPath: null
690
+ };
691
+ }
692
+ const rawConnection = (_a = config.connection) != null ? _a : {};
693
+ const connection = {
694
+ baseUrl: (_b = rawConnection.baseUrl) != null ? _b : process.env.AUTHGATE_BASE_URL,
695
+ apiKey: (_c = rawConnection.apiKey) != null ? _c : process.env.AUTHGATE_API_KEY
696
+ };
697
+ return {
698
+ connection,
699
+ rbacConfigPath: config.rbacConfig ? (0, import_path2.resolve)(cwd, config.rbacConfig) : null,
700
+ billingConfigPath: config.billingConfig ? (0, import_path2.resolve)(cwd, config.billingConfig) : null
701
+ };
702
+ }
703
+ async function loadProjectConfigFile(cwd) {
704
+ var _a;
705
+ let configPath = null;
706
+ for (const filename of CONFIG_FILENAMES2) {
707
+ const candidate = (0, import_path2.resolve)(cwd, filename);
708
+ if ((0, import_fs2.existsSync)(candidate)) {
709
+ configPath = candidate;
710
+ break;
711
+ }
712
+ }
713
+ if (!configPath) return null;
714
+ const { createJiti } = await import("jiti");
715
+ const jiti = createJiti(cwd, { interopDefault: true });
716
+ const mod = await jiti.import(configPath);
717
+ const config = (_a = mod.default) != null ? _a : mod;
718
+ return config;
719
+ }
720
+ async function autoLoadEnvFiles(cwd) {
721
+ let dotenv;
722
+ try {
723
+ dotenv = await import("dotenv");
724
+ } catch (e) {
725
+ return;
726
+ }
727
+ for (const file of AUTO_ENV_FILES) {
728
+ const filePath = (0, import_path2.resolve)(cwd, file);
729
+ if ((0, import_fs2.existsSync)(filePath)) {
730
+ dotenv.config({ path: filePath });
731
+ }
732
+ }
733
+ }
734
+
643
735
  // src/cli.ts
644
736
  var import_chalk2 = __toESM(require("chalk"), 1);
645
- var import_fs2 = require("fs");
646
- var import_path2 = require("path");
737
+ var import_fs3 = require("fs");
738
+ var import_path3 = require("path");
647
739
  var HELP = `
648
740
  Usage: @auth-gate/rbac <command> [options]
649
741
 
@@ -658,7 +750,14 @@ Options (sync):
658
750
  --strict Treat warnings as errors (recommended for CI/CD)
659
751
  --json Output diff as JSON
660
752
 
661
- Environment:
753
+ Options (init):
754
+ --config Generate a starter authgate.config.ts instead
755
+
756
+ Config file:
757
+ If authgate.config.ts exists, env vars and connection details are
758
+ loaded from it automatically. See defineConfig() for the API.
759
+
760
+ Environment (when no authgate.config.ts):
662
761
  AUTHGATE_API_KEY Your project API key (required for sync)
663
762
  AUTHGATE_BASE_URL AuthGate instance URL (required for sync)
664
763
  `;
@@ -670,7 +769,8 @@ async function main() {
670
769
  process.exit(0);
671
770
  }
672
771
  if (command === "init") {
673
- await runInit();
772
+ const configFlag = args.includes("--config");
773
+ await runInit({ config: configFlag });
674
774
  return;
675
775
  }
676
776
  if (command === "check") {
@@ -689,9 +789,29 @@ async function main() {
689
789
  console.log(HELP);
690
790
  process.exit(1);
691
791
  }
692
- async function runInit() {
693
- const configPath = (0, import_path2.resolve)(process.cwd(), "authgate.rbac.ts");
694
- if ((0, import_fs2.existsSync)(configPath)) {
792
+ async function runInit(opts) {
793
+ if (opts.config) {
794
+ const configPath2 = (0, import_path3.resolve)(process.cwd(), "authgate.config.ts");
795
+ if ((0, import_fs3.existsSync)(configPath2)) {
796
+ console.error(import_chalk2.default.yellow(`Config file already exists: ${configPath2}`));
797
+ process.exit(1);
798
+ }
799
+ const template2 = `import { defineConfig } from "@auth-gate/core";
800
+
801
+ export default defineConfig({
802
+ connection: {
803
+ baseUrl: process.env.AUTHGATE_URL,
804
+ apiKey: process.env.AUTHGATE_API_KEY,
805
+ },
806
+ });
807
+ `;
808
+ (0, import_fs3.writeFileSync)(configPath2, template2, "utf-8");
809
+ console.log(import_chalk2.default.green(`Created ${configPath2}`));
810
+ console.log(import_chalk2.default.dim("Edit connection details, then run: npx @auth-gate/rbac sync"));
811
+ return;
812
+ }
813
+ const configPath = (0, import_path3.resolve)(process.cwd(), "authgate.rbac.ts");
814
+ if ((0, import_fs3.existsSync)(configPath)) {
695
815
  console.error(import_chalk2.default.yellow(`Config file already exists: ${configPath}`));
696
816
  process.exit(1);
697
817
  }
@@ -726,6 +846,7 @@ export const rbac = defineRbac({
726
846
  },
727
847
  viewer: {
728
848
  name: "Viewer",
849
+ isDefault: true,
729
850
  grants: {
730
851
  documents: { read: true },
731
852
  },
@@ -736,14 +857,16 @@ export const rbac = defineRbac({
736
857
  // Default export for CLI compatibility
737
858
  export default rbac;
738
859
  `;
739
- (0, import_fs2.writeFileSync)(configPath, template, "utf-8");
860
+ (0, import_fs3.writeFileSync)(configPath, template, "utf-8");
740
861
  console.log(import_chalk2.default.green(`Created ${configPath}`));
741
862
  console.log(import_chalk2.default.dim("Edit your resources and roles, then run: npx @auth-gate/rbac sync"));
742
863
  }
743
864
  async function runCheck() {
865
+ const cwd = process.cwd();
866
+ const projectConfig = await resolveProjectConfig(cwd);
744
867
  let config;
745
868
  try {
746
- config = await loadRbacConfig(process.cwd());
869
+ config = projectConfig.rbacConfigPath ? await loadRbacConfigFromPath(projectConfig.rbacConfigPath, cwd) : await loadRbacConfig(cwd);
747
870
  } catch (err) {
748
871
  console.error(import_chalk2.default.red(`Config error: ${err.message}`));
749
872
  process.exit(1);
@@ -758,19 +881,24 @@ async function runCheck() {
758
881
  console.log(import_chalk2.default.dim(`${resourceCount} resource${resourceCount > 1 ? "s" : ""}, ${roleCount} role${roleCount > 1 ? "s" : ""}, ${permCount} permission${permCount > 1 ? "s" : ""}`));
759
882
  }
760
883
  async function runSync(opts) {
761
- const apiKey = process.env.AUTHGATE_API_KEY;
762
- const baseUrl = process.env.AUTHGATE_BASE_URL;
884
+ const cwd = process.cwd();
885
+ const projectConfig = await resolveProjectConfig(cwd);
886
+ const { baseUrl, apiKey } = projectConfig.connection;
763
887
  if (!apiKey) {
764
- console.error(import_chalk2.default.red("Missing AUTHGATE_API_KEY environment variable."));
888
+ console.error(import_chalk2.default.red(
889
+ "Missing API key. Set AUTHGATE_API_KEY or configure connection in authgate.config.ts."
890
+ ));
765
891
  process.exit(1);
766
892
  }
767
893
  if (!baseUrl) {
768
- console.error(import_chalk2.default.red("Missing AUTHGATE_BASE_URL environment variable."));
894
+ console.error(import_chalk2.default.red(
895
+ "Missing base URL. Set AUTHGATE_BASE_URL or configure connection in authgate.config.ts."
896
+ ));
769
897
  process.exit(1);
770
898
  }
771
899
  let config;
772
900
  try {
773
- config = await loadRbacConfig(process.cwd());
901
+ config = projectConfig.rbacConfigPath ? await loadRbacConfigFromPath(projectConfig.rbacConfigPath, cwd) : await loadRbacConfig(cwd);
774
902
  } catch (err) {
775
903
  console.error(import_chalk2.default.red(`Config error: ${err.message}`));
776
904
  process.exit(1);
package/dist/cli.mjs CHANGED
@@ -4,13 +4,80 @@ import {
4
4
  __spreadValues,
5
5
  computeRbacDiff,
6
6
  formatRbacDiff,
7
- loadRbacConfig
8
- } from "./chunk-5YEMXO7B.mjs";
7
+ loadRbacConfig,
8
+ loadRbacConfigFromPath
9
+ } from "./chunk-5CSBBETH.mjs";
10
+
11
+ // src/project-config-loader.ts
12
+ import { resolve } from "path";
13
+ import { existsSync } from "fs";
14
+ var CONFIG_FILENAMES = [
15
+ "authgate.config.ts",
16
+ "authgate.config.js",
17
+ "authgate.config.mjs"
18
+ ];
19
+ var AUTO_ENV_FILES = [".env", ".env.local"];
20
+ async function resolveProjectConfig(cwd) {
21
+ var _a, _b, _c;
22
+ await autoLoadEnvFiles(cwd);
23
+ const config = await loadProjectConfigFile(cwd);
24
+ if (!config) {
25
+ return {
26
+ connection: {
27
+ baseUrl: process.env.AUTHGATE_BASE_URL,
28
+ apiKey: process.env.AUTHGATE_API_KEY
29
+ },
30
+ rbacConfigPath: null,
31
+ billingConfigPath: null
32
+ };
33
+ }
34
+ const rawConnection = (_a = config.connection) != null ? _a : {};
35
+ const connection = {
36
+ baseUrl: (_b = rawConnection.baseUrl) != null ? _b : process.env.AUTHGATE_BASE_URL,
37
+ apiKey: (_c = rawConnection.apiKey) != null ? _c : process.env.AUTHGATE_API_KEY
38
+ };
39
+ return {
40
+ connection,
41
+ rbacConfigPath: config.rbacConfig ? resolve(cwd, config.rbacConfig) : null,
42
+ billingConfigPath: config.billingConfig ? resolve(cwd, config.billingConfig) : null
43
+ };
44
+ }
45
+ async function loadProjectConfigFile(cwd) {
46
+ var _a;
47
+ let configPath = null;
48
+ for (const filename of CONFIG_FILENAMES) {
49
+ const candidate = resolve(cwd, filename);
50
+ if (existsSync(candidate)) {
51
+ configPath = candidate;
52
+ break;
53
+ }
54
+ }
55
+ if (!configPath) return null;
56
+ const { createJiti } = await import("jiti");
57
+ const jiti = createJiti(cwd, { interopDefault: true });
58
+ const mod = await jiti.import(configPath);
59
+ const config = (_a = mod.default) != null ? _a : mod;
60
+ return config;
61
+ }
62
+ async function autoLoadEnvFiles(cwd) {
63
+ let dotenv;
64
+ try {
65
+ dotenv = await import("dotenv");
66
+ } catch (e) {
67
+ return;
68
+ }
69
+ for (const file of AUTO_ENV_FILES) {
70
+ const filePath = resolve(cwd, file);
71
+ if (existsSync(filePath)) {
72
+ dotenv.config({ path: filePath });
73
+ }
74
+ }
75
+ }
9
76
 
10
77
  // src/cli.ts
11
78
  import chalk from "chalk";
12
- import { writeFileSync, existsSync } from "fs";
13
- import { resolve } from "path";
79
+ import { writeFileSync, existsSync as existsSync2 } from "fs";
80
+ import { resolve as resolve2 } from "path";
14
81
  var HELP = `
15
82
  Usage: @auth-gate/rbac <command> [options]
16
83
 
@@ -25,7 +92,14 @@ Options (sync):
25
92
  --strict Treat warnings as errors (recommended for CI/CD)
26
93
  --json Output diff as JSON
27
94
 
28
- Environment:
95
+ Options (init):
96
+ --config Generate a starter authgate.config.ts instead
97
+
98
+ Config file:
99
+ If authgate.config.ts exists, env vars and connection details are
100
+ loaded from it automatically. See defineConfig() for the API.
101
+
102
+ Environment (when no authgate.config.ts):
29
103
  AUTHGATE_API_KEY Your project API key (required for sync)
30
104
  AUTHGATE_BASE_URL AuthGate instance URL (required for sync)
31
105
  `;
@@ -37,7 +111,8 @@ async function main() {
37
111
  process.exit(0);
38
112
  }
39
113
  if (command === "init") {
40
- await runInit();
114
+ const configFlag = args.includes("--config");
115
+ await runInit({ config: configFlag });
41
116
  return;
42
117
  }
43
118
  if (command === "check") {
@@ -56,9 +131,29 @@ async function main() {
56
131
  console.log(HELP);
57
132
  process.exit(1);
58
133
  }
59
- async function runInit() {
60
- const configPath = resolve(process.cwd(), "authgate.rbac.ts");
61
- if (existsSync(configPath)) {
134
+ async function runInit(opts) {
135
+ if (opts.config) {
136
+ const configPath2 = resolve2(process.cwd(), "authgate.config.ts");
137
+ if (existsSync2(configPath2)) {
138
+ console.error(chalk.yellow(`Config file already exists: ${configPath2}`));
139
+ process.exit(1);
140
+ }
141
+ const template2 = `import { defineConfig } from "@auth-gate/core";
142
+
143
+ export default defineConfig({
144
+ connection: {
145
+ baseUrl: process.env.AUTHGATE_URL,
146
+ apiKey: process.env.AUTHGATE_API_KEY,
147
+ },
148
+ });
149
+ `;
150
+ writeFileSync(configPath2, template2, "utf-8");
151
+ console.log(chalk.green(`Created ${configPath2}`));
152
+ console.log(chalk.dim("Edit connection details, then run: npx @auth-gate/rbac sync"));
153
+ return;
154
+ }
155
+ const configPath = resolve2(process.cwd(), "authgate.rbac.ts");
156
+ if (existsSync2(configPath)) {
62
157
  console.error(chalk.yellow(`Config file already exists: ${configPath}`));
63
158
  process.exit(1);
64
159
  }
@@ -93,6 +188,7 @@ export const rbac = defineRbac({
93
188
  },
94
189
  viewer: {
95
190
  name: "Viewer",
191
+ isDefault: true,
96
192
  grants: {
97
193
  documents: { read: true },
98
194
  },
@@ -108,9 +204,11 @@ export default rbac;
108
204
  console.log(chalk.dim("Edit your resources and roles, then run: npx @auth-gate/rbac sync"));
109
205
  }
110
206
  async function runCheck() {
207
+ const cwd = process.cwd();
208
+ const projectConfig = await resolveProjectConfig(cwd);
111
209
  let config;
112
210
  try {
113
- config = await loadRbacConfig(process.cwd());
211
+ config = projectConfig.rbacConfigPath ? await loadRbacConfigFromPath(projectConfig.rbacConfigPath, cwd) : await loadRbacConfig(cwd);
114
212
  } catch (err) {
115
213
  console.error(chalk.red(`Config error: ${err.message}`));
116
214
  process.exit(1);
@@ -125,19 +223,24 @@ async function runCheck() {
125
223
  console.log(chalk.dim(`${resourceCount} resource${resourceCount > 1 ? "s" : ""}, ${roleCount} role${roleCount > 1 ? "s" : ""}, ${permCount} permission${permCount > 1 ? "s" : ""}`));
126
224
  }
127
225
  async function runSync(opts) {
128
- const apiKey = process.env.AUTHGATE_API_KEY;
129
- const baseUrl = process.env.AUTHGATE_BASE_URL;
226
+ const cwd = process.cwd();
227
+ const projectConfig = await resolveProjectConfig(cwd);
228
+ const { baseUrl, apiKey } = projectConfig.connection;
130
229
  if (!apiKey) {
131
- console.error(chalk.red("Missing AUTHGATE_API_KEY environment variable."));
230
+ console.error(chalk.red(
231
+ "Missing API key. Set AUTHGATE_API_KEY or configure connection in authgate.config.ts."
232
+ ));
132
233
  process.exit(1);
133
234
  }
134
235
  if (!baseUrl) {
135
- console.error(chalk.red("Missing AUTHGATE_BASE_URL environment variable."));
236
+ console.error(chalk.red(
237
+ "Missing base URL. Set AUTHGATE_BASE_URL or configure connection in authgate.config.ts."
238
+ ));
136
239
  process.exit(1);
137
240
  }
138
241
  let config;
139
242
  try {
140
- config = await loadRbacConfig(process.cwd());
243
+ config = projectConfig.rbacConfigPath ? await loadRbacConfigFromPath(projectConfig.rbacConfigPath, cwd) : await loadRbacConfig(cwd);
141
244
  } catch (err) {
142
245
  console.error(chalk.red(`Config error: ${err.message}`));
143
246
  process.exit(1);
package/dist/index.cjs CHANGED
@@ -32,10 +32,13 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  RbacSyncClient: () => RbacSyncClient,
34
34
  computeRbacDiff: () => computeRbacDiff,
35
+ createRoleManagement: () => createRoleManagement,
36
+ defineConfig: () => import_core.defineConfig,
35
37
  defineRbac: () => defineRbac,
36
38
  formatRbacDiff: () => formatRbacDiff,
37
39
  hashConditionSource: () => hashConditionSource,
38
40
  loadRbacConfig: () => loadRbacConfig,
41
+ loadRbacConfigFromPath: () => loadRbacConfigFromPath,
39
42
  validateRbacConfig: () => validateRbacConfig
40
43
  });
41
44
  module.exports = __toCommonJS(index_exports);
@@ -169,6 +172,21 @@ function validateRbacConfig(config) {
169
172
  }
170
173
  }
171
174
  }
175
+ const defaultRoles = [];
176
+ for (const key of roleKeys) {
177
+ const role = roles[key];
178
+ if (role.isDefault !== void 0 && typeof role.isDefault !== "boolean") {
179
+ throw new Error(`Role "${key}".isDefault must be a boolean.`);
180
+ }
181
+ if (role.isDefault === true) {
182
+ defaultRoles.push(key);
183
+ }
184
+ }
185
+ if (defaultRoles.length > 1) {
186
+ throw new Error(
187
+ `Only one role may have isDefault: true. Found ${defaultRoles.length}: ${defaultRoles.join(", ")}.`
188
+ );
189
+ }
172
190
  for (const key of roleKeys) {
173
191
  const role = roles[key];
174
192
  if (role.inherits !== void 0) {
@@ -277,7 +295,6 @@ var CONFIG_FILENAMES = [
277
295
  "authgate.rbac.mjs"
278
296
  ];
279
297
  async function loadRbacConfig(cwd) {
280
- var _a, _b;
281
298
  let configPath = null;
282
299
  for (const filename of CONFIG_FILENAMES) {
283
300
  const candidate = (0, import_path.resolve)(cwd, filename);
@@ -292,8 +309,12 @@ async function loadRbacConfig(cwd) {
292
309
  Run \`npx @auth-gate/rbac init\` to create one.`
293
310
  );
294
311
  }
312
+ return loadRbacConfigFromPath(configPath, cwd);
313
+ }
314
+ async function loadRbacConfigFromPath(configPath, cwd) {
315
+ var _a, _b;
295
316
  const { createJiti } = await import("jiti");
296
- const jiti = createJiti(cwd, { interopDefault: true });
317
+ const jiti = createJiti(cwd != null ? cwd : process.cwd(), { interopDefault: true });
297
318
  const mod = await jiti.import(configPath);
298
319
  const raw = (_b = (_a = mod.default) != null ? _a : mod.rbac) != null ? _b : mod;
299
320
  const config = raw && typeof raw === "object" && "_config" in raw ? raw._config : raw;
@@ -321,7 +342,7 @@ function hashConditionSource(fn) {
321
342
  return createHash("sha256").update(fn.toString()).digest("hex").slice(0, 16);
322
343
  }
323
344
  function computeRbacDiff(config, server, memberCounts) {
324
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
345
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
325
346
  const resourceOps = [];
326
347
  const roleOps = [];
327
348
  const conditionOps = [];
@@ -412,6 +433,11 @@ function computeRbacDiff(config, server, memberCounts) {
412
433
  if (existingInherits !== configInherits) {
413
434
  changes.push("inherits changed");
414
435
  }
436
+ const existingDefault = (_l = existing.isDefault) != null ? _l : false;
437
+ const configDefault = (_m = role.isDefault) != null ? _m : false;
438
+ if (existingDefault !== configDefault) {
439
+ changes.push(`isDefault: ${existingDefault} -> ${configDefault}`);
440
+ }
415
441
  if (changes.length > 0) {
416
442
  roleOps.push({ type: "update", key, role, existing, changes });
417
443
  }
@@ -421,7 +447,7 @@ function computeRbacDiff(config, server, memberCounts) {
421
447
  for (const [key, role] of serverRoleByKey) {
422
448
  if (renameMap.has(key)) continue;
423
449
  if (role.isActive) {
424
- const members = (_l = memberCounts[role.configKey]) != null ? _l : 0;
450
+ const members = (_n = memberCounts[role.configKey]) != null ? _n : 0;
425
451
  if (members > 0) hasDestructive = true;
426
452
  roleOps.push({ type: "archive", key, existing: role, assignedMembers: members });
427
453
  }
@@ -571,6 +597,9 @@ function formatRbacDiff(diff, dryRun) {
571
597
  import_chalk.default.dim(` inherits: [${[...op.role.inherits].join(", ")}]`)
572
598
  );
573
599
  }
600
+ if (op.role.isDefault) {
601
+ lines.push(import_chalk.default.dim(` isDefault: true`));
602
+ }
574
603
  } else if (op.type === "update") {
575
604
  lines.push(import_chalk.default.yellow(` ~ UPDATE role "${op.key}"`));
576
605
  for (const change of op.changes) {
@@ -641,8 +670,96 @@ function formatRbacDiff(diff, dryRun) {
641
670
  return lines.join("\n");
642
671
  }
643
672
 
673
+ // src/role-management.ts
674
+ function createRoleManagement(config) {
675
+ const baseUrl = config.baseUrl.replace(/\/$/, "");
676
+ const apiKey = config.apiKey;
677
+ async function request(method, path, body) {
678
+ var _a, _b;
679
+ const url = `${baseUrl}${path}`;
680
+ const headers = {
681
+ Authorization: `Bearer ${apiKey}`,
682
+ "Content-Type": "application/json"
683
+ };
684
+ const res = await fetch(url, {
685
+ method,
686
+ headers,
687
+ body: body ? JSON.stringify(body) : void 0
688
+ });
689
+ if (!res.ok) {
690
+ const text = await res.text();
691
+ let message;
692
+ try {
693
+ const json = JSON.parse(text);
694
+ message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : "Unknown error";
695
+ } catch (e) {
696
+ message = text.length > 500 ? text.slice(0, 500) + "..." : text;
697
+ }
698
+ if (apiKey) {
699
+ message = message.replaceAll(apiKey, "[REDACTED]");
700
+ }
701
+ throw new Error(`API error (${res.status}): ${message}`);
702
+ }
703
+ return res.json();
704
+ }
705
+ return {
706
+ async listRoles() {
707
+ const result = await request("GET", "/api/v1/roles");
708
+ return result.data.map(toRole);
709
+ },
710
+ async createRole(input) {
711
+ var _a, _b;
712
+ const result = await request("POST", "/api/v1/roles", {
713
+ key: input.key,
714
+ name: input.name,
715
+ description: input.description,
716
+ is_default: (_a = input.isDefault) != null ? _a : false,
717
+ permissions: (_b = input.permissions) != null ? _b : []
718
+ });
719
+ return toRole(result.role);
720
+ },
721
+ async updateRole(roleId, input) {
722
+ const body = {};
723
+ if (input.name !== void 0) body.name = input.name;
724
+ if (input.description !== void 0) body.description = input.description;
725
+ if (input.permissions !== void 0) body.permissions = input.permissions;
726
+ if (input.isDefault !== void 0) body.is_default = input.isDefault;
727
+ const result = await request(
728
+ "PATCH",
729
+ `/api/v1/roles/${encodeURIComponent(roleId)}`,
730
+ body
731
+ );
732
+ return toRole(result.role);
733
+ },
734
+ async deleteRole(roleId) {
735
+ await request(
736
+ "DELETE",
737
+ `/api/v1/roles/${encodeURIComponent(roleId)}`
738
+ );
739
+ },
740
+ async setDefaultRole(roleId) {
741
+ return this.updateRole(roleId, { isDefault: true });
742
+ }
743
+ };
744
+ }
745
+ function toRole(raw) {
746
+ return {
747
+ id: raw.id,
748
+ projectId: raw.project_id,
749
+ key: raw.key,
750
+ name: raw.name,
751
+ description: raw.description,
752
+ isDefault: raw.is_default,
753
+ permissions: raw.permissions,
754
+ createdAt: raw.created_at,
755
+ updatedAt: raw.updated_at
756
+ };
757
+ }
758
+
644
759
  // src/index.ts
760
+ var import_core = require("@auth-gate/core");
645
761
  function defineRbac(config, opts) {
762
+ var _a;
646
763
  if ((opts == null ? void 0 : opts.validate) !== false) {
647
764
  validateRbacConfig(config);
648
765
  }
@@ -656,7 +773,7 @@ function defineRbac(config, opts) {
656
773
  }
657
774
  const roles = {};
658
775
  for (const [key, role] of Object.entries(config.roles)) {
659
- roles[key] = { key, name: role.name, grants: role.grants };
776
+ roles[key] = { key, name: role.name, grants: role.grants, isDefault: (_a = role.isDefault) != null ? _a : false };
660
777
  }
661
778
  const permissions = {};
662
779
  for (const [key, resource] of Object.entries(config.resources)) {
@@ -679,9 +796,12 @@ function defineRbac(config, opts) {
679
796
  0 && (module.exports = {
680
797
  RbacSyncClient,
681
798
  computeRbacDiff,
799
+ createRoleManagement,
800
+ defineConfig,
682
801
  defineRbac,
683
802
  formatRbacDiff,
684
803
  hashConditionSource,
685
804
  loadRbacConfig,
805
+ loadRbacConfigFromPath,
686
806
  validateRbacConfig
687
807
  });
package/dist/index.d.cts CHANGED
@@ -1,3 +1,5 @@
1
+ export { AuthGateConnection, AuthGateProjectConfig, defineConfig } from '@auth-gate/core';
2
+
1
3
  /** A resource definition: declares available actions and optional scopes. */
2
4
  interface ResourceConfig {
3
5
  actions: readonly string[];
@@ -39,6 +41,8 @@ interface RoleConfig {
39
41
  description?: string;
40
42
  inherits?: readonly string[];
41
43
  grants: Record<string, Record<string, GrantValue>>;
44
+ /** Mark this role as the default for new org members. Only one role may be default. */
45
+ isDefault?: boolean;
42
46
  /** Previous config key if this role was renamed. */
43
47
  renamedFrom?: string;
44
48
  }
@@ -80,6 +84,63 @@ type InferScopes<T, Resource extends string> = T extends {
80
84
  type InferConditionKeys<T> = T extends {
81
85
  conditions: infer C;
82
86
  } ? keyof C & string : never;
87
+ /** Extract role keys that have `isDefault: true`. */
88
+ type DefaultRoleKeys<Roles> = {
89
+ [K in keyof Roles]: Roles[K] extends {
90
+ isDefault: true;
91
+ } ? K : never;
92
+ }[keyof Roles];
93
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
94
+ type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
95
+ /**
96
+ * Compile-time constraint: if more than one role has `isDefault: true`,
97
+ * intersect those roles' `isDefault` with an error string so TypeScript
98
+ * produces a descriptive message.
99
+ *
100
+ * Error reads: Type 'true' is not assignable to type '"ERROR: ..."'.
101
+ */
102
+ type ValidateSingleDefault<T> = T extends {
103
+ roles: infer Roles;
104
+ } ? IsUnion<DefaultRoleKeys<Roles>> extends true ? {
105
+ roles: {
106
+ [K in DefaultRoleKeys<Roles> & keyof Roles]: {
107
+ isDefault: "ERROR: Only one role may have isDefault: true";
108
+ };
109
+ };
110
+ } : {} : {};
111
+ /** Extract valid action string literals from a resource config. */
112
+ type ValidActions<R> = R extends {
113
+ actions: readonly (infer U)[];
114
+ } ? U & string : never;
115
+ /**
116
+ * Compile-time constraint: validates grant resource/action keys against
117
+ * declared resources at the type level.
118
+ *
119
+ * - **Invalid resource key** → descriptive error naming the resource.
120
+ * - **Invalid action key** → descriptive error listing valid actions.
121
+ * - **Valid grants** → passes through original types transparently.
122
+ *
123
+ * Used as `T & ValidateConfig<T>` in the `defineRbac()` parameter type.
124
+ *
125
+ * @note Extra properties on role objects (e.g. `isFakeProp`) are caught
126
+ * by the runtime `validateRbacConfig()` which runs by default.
127
+ */
128
+ type ValidateConfig<T> = T extends {
129
+ resources: infer R extends Record<string, ResourceConfig>;
130
+ roles: infer Roles;
131
+ } ? {
132
+ roles: {
133
+ [RK in keyof Roles]: Roles[RK] extends {
134
+ grants: infer G;
135
+ } ? {
136
+ grants: {
137
+ [ResK in keyof G]: ResK extends keyof R ? keyof G[ResK] extends ValidActions<R[ResK & keyof R]> ? {
138
+ [ActK in keyof G[ResK]]: G[ResK][ActK];
139
+ } : `ERROR: invalid action(s) in "${ResK & string}" grants. Valid actions: ${ValidActions<R[ResK & keyof R]>}` : `ERROR: resource "${ResK & string}" is not declared in resources`;
140
+ };
141
+ } : Roles[RK];
142
+ };
143
+ } : {};
83
144
  /** A structured resource with a `key` constant and an `actions` map of `"resource:action"` strings. */
84
145
  type StructuredResource<T, K extends string> = {
85
146
  readonly key: K;
@@ -91,7 +152,7 @@ type StructuredResource<T, K extends string> = {
91
152
  type StructuredResources<T> = {
92
153
  readonly [K in InferResourceKeys<T>]: StructuredResource<T, K>;
93
154
  };
94
- /** A structured role with `key`, `name`, and `grants` from the config. */
155
+ /** A structured role with `key`, `name`, `grants`, and optional `isDefault` from the config. */
95
156
  type StructuredRole<T, K extends string> = {
96
157
  readonly key: K;
97
158
  readonly name: T extends {
@@ -104,6 +165,11 @@ type StructuredRole<T, K extends string> = {
104
165
  } ? K extends keyof R ? R[K] extends {
105
166
  grants: infer G;
106
167
  } ? Readonly<G> : {} : {} : {};
168
+ readonly isDefault: T extends {
169
+ roles: infer R;
170
+ } ? K extends keyof R ? R[K] extends {
171
+ isDefault: infer D;
172
+ } ? D : false : false : false;
107
173
  };
108
174
  /** Map of all roles as structured objects: `roles.admin.key` -> `"admin"`. */
109
175
  type StructuredRoles<T> = {
@@ -121,7 +187,7 @@ type StructuredPermissions<T> = {
121
187
  * Carries phantom types that downstream factories use to constrain
122
188
  * permission/role string parameters -- enabling full IDE autocomplete.
123
189
  */
124
- interface TypedRbac<T extends RbacConfig> {
190
+ interface TypedRbac<T> {
125
191
  /** The raw RBAC config. Access resources, roles, etc. via `rbac._config`. */
126
192
  readonly _config: T;
127
193
  /**
@@ -155,6 +221,11 @@ type ResourceKey<A> = A extends TypedRbac<infer T> ? InferResourceKeys<T> : stri
155
221
  declare function validateRbacConfig(config: unknown): RbacConfig;
156
222
 
157
223
  declare function loadRbacConfig(cwd: string): Promise<RbacConfig>;
224
+ /**
225
+ * Load RBAC config from a specific file path.
226
+ * Used when `authgate.config.ts` specifies `rbacConfig`.
227
+ */
228
+ declare function loadRbacConfigFromPath(configPath: string, cwd?: string): Promise<RbacConfig>;
158
229
 
159
230
  interface ServerResource {
160
231
  id: string;
@@ -172,6 +243,7 @@ interface ServerRole {
172
243
  grants: Record<string, Record<string, GrantValue>> | null;
173
244
  inherits: string[];
174
245
  resolvedPermissions: string[];
246
+ isDefault: boolean;
175
247
  isActive: boolean;
176
248
  managedBy: string;
177
249
  version: number;
@@ -272,12 +344,61 @@ declare class RbacSyncClient {
272
344
 
273
345
  declare function formatRbacDiff(diff: DiffResult, dryRun: boolean): string;
274
346
 
347
+ interface RoleManagementConfig {
348
+ baseUrl: string;
349
+ apiKey: string;
350
+ }
351
+ interface Role {
352
+ id: string;
353
+ projectId: string;
354
+ key: string;
355
+ name: string;
356
+ description: string | null;
357
+ isDefault: boolean;
358
+ permissions: string[];
359
+ createdAt: string;
360
+ updatedAt: string;
361
+ }
362
+ interface CreateRoleInput {
363
+ key: string;
364
+ name: string;
365
+ description?: string;
366
+ isDefault?: boolean;
367
+ permissions?: string[];
368
+ }
369
+ interface UpdateRoleInput {
370
+ name?: string;
371
+ description?: string | null;
372
+ permissions?: string[];
373
+ isDefault?: boolean;
374
+ }
375
+ interface RoleManagementClient {
376
+ listRoles(): Promise<Role[]>;
377
+ createRole(input: CreateRoleInput): Promise<Role>;
378
+ updateRole(roleId: string, input: UpdateRoleInput): Promise<Role>;
379
+ deleteRole(roleId: string): Promise<void>;
380
+ setDefaultRole(roleId: string): Promise<Role>;
381
+ }
382
+ declare function createRoleManagement(config: RoleManagementConfig): RoleManagementClient;
383
+
384
+ /** Base config shape used as the generic constraint for `defineRbac()`. */
385
+ type DefineRbacConfig = {
386
+ resources: Record<string, ResourceConfig>;
387
+ conditions?: Record<string, ConditionFn>;
388
+ roles: Record<string, {
389
+ name: string;
390
+ description?: string;
391
+ inherits?: readonly string[];
392
+ isDefault?: boolean;
393
+ renamedFrom?: string;
394
+ grants: Record<string, Record<string, GrantValue>>;
395
+ }>;
396
+ };
275
397
  /**
276
398
  * Define your RBAC config. Use this in your `authgate.rbac.ts` config file.
277
399
  *
278
- * Returns a `TypedRbac<T>` object with structured resource, role, and permission
279
- * objects -- enabling full IDE autocomplete via dot-notation across hooks, helpers,
280
- * and server-side checks.
400
+ * Grants are validated at compile-time: if a role references an undeclared
401
+ * resource or action, TypeScript flags it as a type error.
281
402
  *
282
403
  * @example
283
404
  * ```ts
@@ -310,8 +431,8 @@ declare function formatRbacDiff(diff: DiffResult, dryRun: boolean): string;
310
431
  * rbac.permissions.documents.write // "documents:write"
311
432
  * ```
312
433
  */
313
- declare function defineRbac<const T extends RbacConfig>(config: T, opts?: {
434
+ declare function defineRbac<const T extends DefineRbacConfig>(config: T & ValidateConfig<T> & ValidateSingleDefault<T>, opts?: {
314
435
  validate?: boolean;
315
436
  }): TypedRbac<T>;
316
437
 
317
- export { type ApplyResult, type ConditionContext, type ConditionFn, type ConditionOp, type DiffResult, type GrantValue, type InferActions, type InferConditionKeys, type InferPermissions, type InferResourceKeys, type InferRoleKeys, type InferScopes, type Permission, type RbacConfig, RbacSyncClient, type RbacSyncClientConfig, type ResourceConfig, type ResourceKey, type ResourceOp, type RoleConfig, type RoleKey, type RoleOp, type ServerCondition, type ServerResource, type ServerRole, type ServerState, type TypedRbac, computeRbacDiff, defineRbac, formatRbacDiff, hashConditionSource, loadRbacConfig, validateRbacConfig };
438
+ export { type ApplyResult, type ConditionContext, type ConditionFn, type ConditionOp, type CreateRoleInput, type DiffResult, type GrantValue, type InferActions, type InferConditionKeys, type InferPermissions, type InferResourceKeys, type InferRoleKeys, type InferScopes, type Permission, type RbacConfig, RbacSyncClient, type RbacSyncClientConfig, type ResourceConfig, type ResourceKey, type ResourceOp, type Role, type RoleConfig, type RoleKey, type RoleManagementClient, type RoleManagementConfig, type RoleOp, type ServerCondition, type ServerResource, type ServerRole, type ServerState, type TypedRbac, type UpdateRoleInput, computeRbacDiff, createRoleManagement, defineRbac, formatRbacDiff, hashConditionSource, loadRbacConfig, loadRbacConfigFromPath, validateRbacConfig };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { AuthGateConnection, AuthGateProjectConfig, defineConfig } from '@auth-gate/core';
2
+
1
3
  /** A resource definition: declares available actions and optional scopes. */
2
4
  interface ResourceConfig {
3
5
  actions: readonly string[];
@@ -39,6 +41,8 @@ interface RoleConfig {
39
41
  description?: string;
40
42
  inherits?: readonly string[];
41
43
  grants: Record<string, Record<string, GrantValue>>;
44
+ /** Mark this role as the default for new org members. Only one role may be default. */
45
+ isDefault?: boolean;
42
46
  /** Previous config key if this role was renamed. */
43
47
  renamedFrom?: string;
44
48
  }
@@ -80,6 +84,63 @@ type InferScopes<T, Resource extends string> = T extends {
80
84
  type InferConditionKeys<T> = T extends {
81
85
  conditions: infer C;
82
86
  } ? keyof C & string : never;
87
+ /** Extract role keys that have `isDefault: true`. */
88
+ type DefaultRoleKeys<Roles> = {
89
+ [K in keyof Roles]: Roles[K] extends {
90
+ isDefault: true;
91
+ } ? K : never;
92
+ }[keyof Roles];
93
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
94
+ type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
95
+ /**
96
+ * Compile-time constraint: if more than one role has `isDefault: true`,
97
+ * intersect those roles' `isDefault` with an error string so TypeScript
98
+ * produces a descriptive message.
99
+ *
100
+ * Error reads: Type 'true' is not assignable to type '"ERROR: ..."'.
101
+ */
102
+ type ValidateSingleDefault<T> = T extends {
103
+ roles: infer Roles;
104
+ } ? IsUnion<DefaultRoleKeys<Roles>> extends true ? {
105
+ roles: {
106
+ [K in DefaultRoleKeys<Roles> & keyof Roles]: {
107
+ isDefault: "ERROR: Only one role may have isDefault: true";
108
+ };
109
+ };
110
+ } : {} : {};
111
+ /** Extract valid action string literals from a resource config. */
112
+ type ValidActions<R> = R extends {
113
+ actions: readonly (infer U)[];
114
+ } ? U & string : never;
115
+ /**
116
+ * Compile-time constraint: validates grant resource/action keys against
117
+ * declared resources at the type level.
118
+ *
119
+ * - **Invalid resource key** → descriptive error naming the resource.
120
+ * - **Invalid action key** → descriptive error listing valid actions.
121
+ * - **Valid grants** → passes through original types transparently.
122
+ *
123
+ * Used as `T & ValidateConfig<T>` in the `defineRbac()` parameter type.
124
+ *
125
+ * @note Extra properties on role objects (e.g. `isFakeProp`) are caught
126
+ * by the runtime `validateRbacConfig()` which runs by default.
127
+ */
128
+ type ValidateConfig<T> = T extends {
129
+ resources: infer R extends Record<string, ResourceConfig>;
130
+ roles: infer Roles;
131
+ } ? {
132
+ roles: {
133
+ [RK in keyof Roles]: Roles[RK] extends {
134
+ grants: infer G;
135
+ } ? {
136
+ grants: {
137
+ [ResK in keyof G]: ResK extends keyof R ? keyof G[ResK] extends ValidActions<R[ResK & keyof R]> ? {
138
+ [ActK in keyof G[ResK]]: G[ResK][ActK];
139
+ } : `ERROR: invalid action(s) in "${ResK & string}" grants. Valid actions: ${ValidActions<R[ResK & keyof R]>}` : `ERROR: resource "${ResK & string}" is not declared in resources`;
140
+ };
141
+ } : Roles[RK];
142
+ };
143
+ } : {};
83
144
  /** A structured resource with a `key` constant and an `actions` map of `"resource:action"` strings. */
84
145
  type StructuredResource<T, K extends string> = {
85
146
  readonly key: K;
@@ -91,7 +152,7 @@ type StructuredResource<T, K extends string> = {
91
152
  type StructuredResources<T> = {
92
153
  readonly [K in InferResourceKeys<T>]: StructuredResource<T, K>;
93
154
  };
94
- /** A structured role with `key`, `name`, and `grants` from the config. */
155
+ /** A structured role with `key`, `name`, `grants`, and optional `isDefault` from the config. */
95
156
  type StructuredRole<T, K extends string> = {
96
157
  readonly key: K;
97
158
  readonly name: T extends {
@@ -104,6 +165,11 @@ type StructuredRole<T, K extends string> = {
104
165
  } ? K extends keyof R ? R[K] extends {
105
166
  grants: infer G;
106
167
  } ? Readonly<G> : {} : {} : {};
168
+ readonly isDefault: T extends {
169
+ roles: infer R;
170
+ } ? K extends keyof R ? R[K] extends {
171
+ isDefault: infer D;
172
+ } ? D : false : false : false;
107
173
  };
108
174
  /** Map of all roles as structured objects: `roles.admin.key` -> `"admin"`. */
109
175
  type StructuredRoles<T> = {
@@ -121,7 +187,7 @@ type StructuredPermissions<T> = {
121
187
  * Carries phantom types that downstream factories use to constrain
122
188
  * permission/role string parameters -- enabling full IDE autocomplete.
123
189
  */
124
- interface TypedRbac<T extends RbacConfig> {
190
+ interface TypedRbac<T> {
125
191
  /** The raw RBAC config. Access resources, roles, etc. via `rbac._config`. */
126
192
  readonly _config: T;
127
193
  /**
@@ -155,6 +221,11 @@ type ResourceKey<A> = A extends TypedRbac<infer T> ? InferResourceKeys<T> : stri
155
221
  declare function validateRbacConfig(config: unknown): RbacConfig;
156
222
 
157
223
  declare function loadRbacConfig(cwd: string): Promise<RbacConfig>;
224
+ /**
225
+ * Load RBAC config from a specific file path.
226
+ * Used when `authgate.config.ts` specifies `rbacConfig`.
227
+ */
228
+ declare function loadRbacConfigFromPath(configPath: string, cwd?: string): Promise<RbacConfig>;
158
229
 
159
230
  interface ServerResource {
160
231
  id: string;
@@ -172,6 +243,7 @@ interface ServerRole {
172
243
  grants: Record<string, Record<string, GrantValue>> | null;
173
244
  inherits: string[];
174
245
  resolvedPermissions: string[];
246
+ isDefault: boolean;
175
247
  isActive: boolean;
176
248
  managedBy: string;
177
249
  version: number;
@@ -272,12 +344,61 @@ declare class RbacSyncClient {
272
344
 
273
345
  declare function formatRbacDiff(diff: DiffResult, dryRun: boolean): string;
274
346
 
347
+ interface RoleManagementConfig {
348
+ baseUrl: string;
349
+ apiKey: string;
350
+ }
351
+ interface Role {
352
+ id: string;
353
+ projectId: string;
354
+ key: string;
355
+ name: string;
356
+ description: string | null;
357
+ isDefault: boolean;
358
+ permissions: string[];
359
+ createdAt: string;
360
+ updatedAt: string;
361
+ }
362
+ interface CreateRoleInput {
363
+ key: string;
364
+ name: string;
365
+ description?: string;
366
+ isDefault?: boolean;
367
+ permissions?: string[];
368
+ }
369
+ interface UpdateRoleInput {
370
+ name?: string;
371
+ description?: string | null;
372
+ permissions?: string[];
373
+ isDefault?: boolean;
374
+ }
375
+ interface RoleManagementClient {
376
+ listRoles(): Promise<Role[]>;
377
+ createRole(input: CreateRoleInput): Promise<Role>;
378
+ updateRole(roleId: string, input: UpdateRoleInput): Promise<Role>;
379
+ deleteRole(roleId: string): Promise<void>;
380
+ setDefaultRole(roleId: string): Promise<Role>;
381
+ }
382
+ declare function createRoleManagement(config: RoleManagementConfig): RoleManagementClient;
383
+
384
+ /** Base config shape used as the generic constraint for `defineRbac()`. */
385
+ type DefineRbacConfig = {
386
+ resources: Record<string, ResourceConfig>;
387
+ conditions?: Record<string, ConditionFn>;
388
+ roles: Record<string, {
389
+ name: string;
390
+ description?: string;
391
+ inherits?: readonly string[];
392
+ isDefault?: boolean;
393
+ renamedFrom?: string;
394
+ grants: Record<string, Record<string, GrantValue>>;
395
+ }>;
396
+ };
275
397
  /**
276
398
  * Define your RBAC config. Use this in your `authgate.rbac.ts` config file.
277
399
  *
278
- * Returns a `TypedRbac<T>` object with structured resource, role, and permission
279
- * objects -- enabling full IDE autocomplete via dot-notation across hooks, helpers,
280
- * and server-side checks.
400
+ * Grants are validated at compile-time: if a role references an undeclared
401
+ * resource or action, TypeScript flags it as a type error.
281
402
  *
282
403
  * @example
283
404
  * ```ts
@@ -310,8 +431,8 @@ declare function formatRbacDiff(diff: DiffResult, dryRun: boolean): string;
310
431
  * rbac.permissions.documents.write // "documents:write"
311
432
  * ```
312
433
  */
313
- declare function defineRbac<const T extends RbacConfig>(config: T, opts?: {
434
+ declare function defineRbac<const T extends DefineRbacConfig>(config: T & ValidateConfig<T> & ValidateSingleDefault<T>, opts?: {
314
435
  validate?: boolean;
315
436
  }): TypedRbac<T>;
316
437
 
317
- export { type ApplyResult, type ConditionContext, type ConditionFn, type ConditionOp, type DiffResult, type GrantValue, type InferActions, type InferConditionKeys, type InferPermissions, type InferResourceKeys, type InferRoleKeys, type InferScopes, type Permission, type RbacConfig, RbacSyncClient, type RbacSyncClientConfig, type ResourceConfig, type ResourceKey, type ResourceOp, type RoleConfig, type RoleKey, type RoleOp, type ServerCondition, type ServerResource, type ServerRole, type ServerState, type TypedRbac, computeRbacDiff, defineRbac, formatRbacDiff, hashConditionSource, loadRbacConfig, validateRbacConfig };
438
+ export { type ApplyResult, type ConditionContext, type ConditionFn, type ConditionOp, type CreateRoleInput, type DiffResult, type GrantValue, type InferActions, type InferConditionKeys, type InferPermissions, type InferResourceKeys, type InferRoleKeys, type InferScopes, type Permission, type RbacConfig, RbacSyncClient, type RbacSyncClientConfig, type ResourceConfig, type ResourceKey, type ResourceOp, type Role, type RoleConfig, type RoleKey, type RoleManagementClient, type RoleManagementConfig, type RoleOp, type ServerCondition, type ServerResource, type ServerRole, type ServerState, type TypedRbac, type UpdateRoleInput, computeRbacDiff, createRoleManagement, defineRbac, formatRbacDiff, hashConditionSource, loadRbacConfig, loadRbacConfigFromPath, validateRbacConfig };
package/dist/index.mjs CHANGED
@@ -4,11 +4,100 @@ import {
4
4
  formatRbacDiff,
5
5
  hashConditionSource,
6
6
  loadRbacConfig,
7
+ loadRbacConfigFromPath,
7
8
  validateRbacConfig
8
- } from "./chunk-5YEMXO7B.mjs";
9
+ } from "./chunk-5CSBBETH.mjs";
10
+
11
+ // src/role-management.ts
12
+ function createRoleManagement(config) {
13
+ const baseUrl = config.baseUrl.replace(/\/$/, "");
14
+ const apiKey = config.apiKey;
15
+ async function request(method, path, body) {
16
+ var _a, _b;
17
+ const url = `${baseUrl}${path}`;
18
+ const headers = {
19
+ Authorization: `Bearer ${apiKey}`,
20
+ "Content-Type": "application/json"
21
+ };
22
+ const res = await fetch(url, {
23
+ method,
24
+ headers,
25
+ body: body ? JSON.stringify(body) : void 0
26
+ });
27
+ if (!res.ok) {
28
+ const text = await res.text();
29
+ let message;
30
+ try {
31
+ const json = JSON.parse(text);
32
+ message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : "Unknown error";
33
+ } catch (e) {
34
+ message = text.length > 500 ? text.slice(0, 500) + "..." : text;
35
+ }
36
+ if (apiKey) {
37
+ message = message.replaceAll(apiKey, "[REDACTED]");
38
+ }
39
+ throw new Error(`API error (${res.status}): ${message}`);
40
+ }
41
+ return res.json();
42
+ }
43
+ return {
44
+ async listRoles() {
45
+ const result = await request("GET", "/api/v1/roles");
46
+ return result.data.map(toRole);
47
+ },
48
+ async createRole(input) {
49
+ var _a, _b;
50
+ const result = await request("POST", "/api/v1/roles", {
51
+ key: input.key,
52
+ name: input.name,
53
+ description: input.description,
54
+ is_default: (_a = input.isDefault) != null ? _a : false,
55
+ permissions: (_b = input.permissions) != null ? _b : []
56
+ });
57
+ return toRole(result.role);
58
+ },
59
+ async updateRole(roleId, input) {
60
+ const body = {};
61
+ if (input.name !== void 0) body.name = input.name;
62
+ if (input.description !== void 0) body.description = input.description;
63
+ if (input.permissions !== void 0) body.permissions = input.permissions;
64
+ if (input.isDefault !== void 0) body.is_default = input.isDefault;
65
+ const result = await request(
66
+ "PATCH",
67
+ `/api/v1/roles/${encodeURIComponent(roleId)}`,
68
+ body
69
+ );
70
+ return toRole(result.role);
71
+ },
72
+ async deleteRole(roleId) {
73
+ await request(
74
+ "DELETE",
75
+ `/api/v1/roles/${encodeURIComponent(roleId)}`
76
+ );
77
+ },
78
+ async setDefaultRole(roleId) {
79
+ return this.updateRole(roleId, { isDefault: true });
80
+ }
81
+ };
82
+ }
83
+ function toRole(raw) {
84
+ return {
85
+ id: raw.id,
86
+ projectId: raw.project_id,
87
+ key: raw.key,
88
+ name: raw.name,
89
+ description: raw.description,
90
+ isDefault: raw.is_default,
91
+ permissions: raw.permissions,
92
+ createdAt: raw.created_at,
93
+ updatedAt: raw.updated_at
94
+ };
95
+ }
9
96
 
10
97
  // src/index.ts
98
+ import { defineConfig } from "@auth-gate/core";
11
99
  function defineRbac(config, opts) {
100
+ var _a;
12
101
  if ((opts == null ? void 0 : opts.validate) !== false) {
13
102
  validateRbacConfig(config);
14
103
  }
@@ -22,7 +111,7 @@ function defineRbac(config, opts) {
22
111
  }
23
112
  const roles = {};
24
113
  for (const [key, role] of Object.entries(config.roles)) {
25
- roles[key] = { key, name: role.name, grants: role.grants };
114
+ roles[key] = { key, name: role.name, grants: role.grants, isDefault: (_a = role.isDefault) != null ? _a : false };
26
115
  }
27
116
  const permissions = {};
28
117
  for (const [key, resource] of Object.entries(config.resources)) {
@@ -44,9 +133,12 @@ function defineRbac(config, opts) {
44
133
  export {
45
134
  RbacSyncClient,
46
135
  computeRbacDiff,
136
+ createRoleManagement,
137
+ defineConfig,
47
138
  defineRbac,
48
139
  formatRbacDiff,
49
140
  hashConditionSource,
50
141
  loadRbacConfig,
142
+ loadRbacConfigFromPath,
51
143
  validateRbacConfig
52
144
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auth-gate/rbac",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -20,7 +20,9 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "chalk": "^5.4.0",
23
- "jiti": "^2.4.0"
23
+ "dotenv": "^17.2.4",
24
+ "jiti": "^2.4.0",
25
+ "@auth-gate/core": "0.9.1"
24
26
  },
25
27
  "devDependencies": {
26
28
  "tsup": "^8.0.0",