@auth-gate/rbac 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs ADDED
@@ -0,0 +1,789 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
11
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
12
+ var __spreadValues = (a, b) => {
13
+ for (var prop in b || (b = {}))
14
+ if (__hasOwnProp.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ if (__getOwnPropSymbols)
17
+ for (var prop of __getOwnPropSymbols(b)) {
18
+ if (__propIsEnum.call(b, prop))
19
+ __defNormalProp(a, prop, b[prop]);
20
+ }
21
+ return a;
22
+ };
23
+ var __copyProps = (to, from, except, desc) => {
24
+ if (from && typeof from === "object" || typeof from === "function") {
25
+ for (let key of __getOwnPropNames(from))
26
+ if (!__hasOwnProp.call(to, key) && key !== except)
27
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
28
+ }
29
+ return to;
30
+ };
31
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
32
+ // If the importer is in node compatibility mode or this is not an ESM
33
+ // file that has been converted to a CommonJS file using a Babel-
34
+ // compatible transform (i.e. "__esModule" has not been set), then set
35
+ // "default" to the CommonJS "module.exports" for node compatibility.
36
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
37
+ mod
38
+ ));
39
+
40
+ // src/config-loader.ts
41
+ var import_path = require("path");
42
+ var import_fs = require("fs");
43
+
44
+ // src/validator.ts
45
+ var KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
46
+ function validateRbacConfig(config) {
47
+ if (!config || typeof config !== "object") {
48
+ throw new Error(
49
+ "RBAC config must be an object. Did you forget `export default defineRbac({ ... })`?"
50
+ );
51
+ }
52
+ const c = config;
53
+ if (!c.resources || typeof c.resources !== "object" || Array.isArray(c.resources)) {
54
+ throw new Error("RBAC config must have a `resources` object.");
55
+ }
56
+ if (!c.roles || typeof c.roles !== "object" || Array.isArray(c.roles)) {
57
+ throw new Error("RBAC config must have a `roles` object.");
58
+ }
59
+ const resources = c.resources;
60
+ const roles = c.roles;
61
+ const resourceKeys = Object.keys(resources);
62
+ const roleKeys = Object.keys(roles);
63
+ if (resourceKeys.length === 0) {
64
+ throw new Error("RBAC config must define at least one resource.");
65
+ }
66
+ if (roleKeys.length === 0) {
67
+ throw new Error("RBAC config must define at least one role.");
68
+ }
69
+ const resourceActions = /* @__PURE__ */ new Map();
70
+ for (const key of resourceKeys) {
71
+ if (!KEY_PATTERN.test(key)) {
72
+ throw new Error(
73
+ `Resource key "${key}" is invalid. Use lowercase alphanumeric with underscores (e.g., "documents", "billing_plans").`
74
+ );
75
+ }
76
+ const resource = resources[key];
77
+ if (!resource || typeof resource !== "object") {
78
+ throw new Error(`Resource "${key}" must be an object.`);
79
+ }
80
+ if (!Array.isArray(resource.actions) || resource.actions.length === 0) {
81
+ throw new Error(
82
+ `Resource "${key}" must have a non-empty "actions" array.`
83
+ );
84
+ }
85
+ for (let i = 0; i < resource.actions.length; i++) {
86
+ if (typeof resource.actions[i] !== "string") {
87
+ throw new Error(
88
+ `Resource "${key}".actions[${i}] must be a string.`
89
+ );
90
+ }
91
+ }
92
+ resourceActions.set(key, new Set(resource.actions));
93
+ if (resource.scopes !== void 0) {
94
+ if (!Array.isArray(resource.scopes)) {
95
+ throw new Error(`Resource "${key}".scopes must be an array of strings.`);
96
+ }
97
+ for (let i = 0; i < resource.scopes.length; i++) {
98
+ if (typeof resource.scopes[i] !== "string") {
99
+ throw new Error(`Resource "${key}".scopes[${i}] must be a string.`);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ for (const key of roleKeys) {
105
+ if (!KEY_PATTERN.test(key)) {
106
+ throw new Error(
107
+ `Role key "${key}" is invalid. Use lowercase alphanumeric with underscores (e.g., "admin", "org_owner").`
108
+ );
109
+ }
110
+ const role = roles[key];
111
+ if (!role || typeof role !== "object") {
112
+ throw new Error(`Role "${key}" must be an object.`);
113
+ }
114
+ if (!role.name || typeof role.name !== "string") {
115
+ throw new Error(`Role "${key}" must have a "name" string.`);
116
+ }
117
+ if (!role.grants || typeof role.grants !== "object" || Array.isArray(role.grants)) {
118
+ throw new Error(`Role "${key}" must have a "grants" object.`);
119
+ }
120
+ const grants = role.grants;
121
+ for (const resourceKey of Object.keys(grants)) {
122
+ if (!resourceActions.has(resourceKey)) {
123
+ throw new Error(
124
+ `Role "${key}".grants references undeclared resource "${resourceKey}". Declared resources: ${[...resourceActions.keys()].join(", ")}.`
125
+ );
126
+ }
127
+ const actionsGrant = grants[resourceKey];
128
+ if (!actionsGrant || typeof actionsGrant !== "object" || Array.isArray(actionsGrant)) {
129
+ throw new Error(
130
+ `Role "${key}".grants.${resourceKey} must be an object mapping actions to grant values.`
131
+ );
132
+ }
133
+ const declaredActions = resourceActions.get(resourceKey);
134
+ for (const actionKey of Object.keys(actionsGrant)) {
135
+ if (!declaredActions.has(actionKey)) {
136
+ throw new Error(
137
+ `Role "${key}".grants.${resourceKey} references undeclared action "${actionKey}". Declared actions for "${resourceKey}": ${[...declaredActions].join(", ")}.`
138
+ );
139
+ }
140
+ const value = actionsGrant[actionKey];
141
+ if (value !== true && typeof value !== "string" && !(value && typeof value === "object" && "when" in value && typeof value.when === "string")) {
142
+ throw new Error(
143
+ `Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a scope string, or { when: string }.`
144
+ );
145
+ }
146
+ }
147
+ }
148
+ }
149
+ for (const key of roleKeys) {
150
+ const role = roles[key];
151
+ if (role.inherits !== void 0) {
152
+ if (!Array.isArray(role.inherits)) {
153
+ throw new Error(`Role "${key}".inherits must be an array of role key strings.`);
154
+ }
155
+ for (const parent of role.inherits) {
156
+ if (typeof parent !== "string") {
157
+ throw new Error(`Role "${key}".inherits contains a non-string value.`);
158
+ }
159
+ if (!roleKeys.includes(parent)) {
160
+ throw new Error(
161
+ `Role "${key}".inherits references unknown role "${parent}". Declared roles: ${roleKeys.join(", ")}.`
162
+ );
163
+ }
164
+ }
165
+ }
166
+ }
167
+ function detectCycle(roleKey) {
168
+ const visited = /* @__PURE__ */ new Set();
169
+ function dfs(current, path) {
170
+ if (visited.has(current)) {
171
+ const cycleStart = path.indexOf(current);
172
+ const cycle = [...path.slice(cycleStart), current].join(" -> ");
173
+ throw new Error(
174
+ `Circular inheritance detected: ${cycle}. A role cannot directly or indirectly inherit from itself.`
175
+ );
176
+ }
177
+ visited.add(current);
178
+ const role = roles[current];
179
+ if ((role == null ? void 0 : role.inherits) && Array.isArray(role.inherits)) {
180
+ for (const parent of role.inherits) {
181
+ dfs(parent, [...path, current]);
182
+ }
183
+ }
184
+ visited.delete(current);
185
+ }
186
+ dfs(roleKey, []);
187
+ }
188
+ for (const key of roleKeys) {
189
+ detectCycle(key);
190
+ }
191
+ const renameTargets = /* @__PURE__ */ new Map();
192
+ for (const key of roleKeys) {
193
+ const role = roles[key];
194
+ if (role.renamedFrom !== void 0) {
195
+ if (typeof role.renamedFrom !== "string") {
196
+ throw new Error(`Role "${key}".renamedFrom must be a string.`);
197
+ }
198
+ if (role.renamedFrom === key) {
199
+ throw new Error(`Role "${key}".renamedFrom cannot equal its own key.`);
200
+ }
201
+ if (roleKeys.includes(role.renamedFrom)) {
202
+ throw new Error(
203
+ `Role "${key}".renamedFrom "${role.renamedFrom}" cannot reference an existing role key.`
204
+ );
205
+ }
206
+ if (renameTargets.has(role.renamedFrom)) {
207
+ throw new Error(
208
+ `renamedFrom "${role.renamedFrom}" is claimed by multiple roles: "${renameTargets.get(role.renamedFrom)}" and "${key}".`
209
+ );
210
+ }
211
+ renameTargets.set(role.renamedFrom, key);
212
+ }
213
+ }
214
+ if (c.conditions !== void 0) {
215
+ if (typeof c.conditions !== "object" || Array.isArray(c.conditions) || c.conditions === null) {
216
+ throw new Error("RBAC config `conditions` must be an object mapping names to functions.");
217
+ }
218
+ const conditions = c.conditions;
219
+ for (const [condKey, condValue] of Object.entries(conditions)) {
220
+ if (typeof condValue !== "function") {
221
+ throw new Error(
222
+ `Condition "${condKey}" must be a function. Got ${typeof condValue}.`
223
+ );
224
+ }
225
+ }
226
+ }
227
+ return config;
228
+ }
229
+
230
+ // src/config-loader.ts
231
+ var CONFIG_FILENAMES = [
232
+ "authgate.rbac.ts",
233
+ "authgate.rbac.js",
234
+ "authgate.rbac.mjs"
235
+ ];
236
+ async function loadRbacConfig(cwd) {
237
+ var _a, _b;
238
+ let configPath = null;
239
+ for (const filename of CONFIG_FILENAMES) {
240
+ const candidate = (0, import_path.resolve)(cwd, filename);
241
+ if ((0, import_fs.existsSync)(candidate)) {
242
+ configPath = candidate;
243
+ break;
244
+ }
245
+ }
246
+ if (!configPath) {
247
+ throw new Error(
248
+ `No RBAC config found. Expected one of: ${CONFIG_FILENAMES.join(", ")}
249
+ Run \`npx @auth-gate/rbac init\` to create one.`
250
+ );
251
+ }
252
+ const { createJiti } = await import("jiti");
253
+ const jiti = createJiti(cwd, { interopDefault: true });
254
+ const mod = await jiti.import(configPath);
255
+ const raw = (_b = (_a = mod.default) != null ? _a : mod.rbac) != null ? _b : mod;
256
+ const config = raw && typeof raw === "object" && "_config" in raw ? raw._config : raw;
257
+ return validateRbacConfig(config);
258
+ }
259
+
260
+ // src/diff.ts
261
+ function hashConditionSource(fn) {
262
+ const src = fn.toString();
263
+ let hash = 0;
264
+ for (let i = 0; i < src.length; i++) {
265
+ const ch = src.charCodeAt(i);
266
+ hash = (hash << 5) - hash + ch | 0;
267
+ }
268
+ return hash.toString(36);
269
+ }
270
+ function computeRbacDiff(config, server, memberCounts) {
271
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
272
+ const resourceOps = [];
273
+ const roleOps = [];
274
+ const conditionOps = [];
275
+ let hasDestructive = false;
276
+ const serverResourceByKey = /* @__PURE__ */ new Map();
277
+ for (const res of server.resources) {
278
+ if (res.managedBy === "config") {
279
+ serverResourceByKey.set(res.key, res);
280
+ }
281
+ }
282
+ for (const [key, resource] of Object.entries(config.resources)) {
283
+ const existing = serverResourceByKey.get(key);
284
+ if (!existing) {
285
+ resourceOps.push({ type: "create", key, resource });
286
+ } else {
287
+ const changes = [];
288
+ const existingActions = [...existing.actions].sort().join(",");
289
+ const configActions = [...resource.actions].sort().join(",");
290
+ if (existingActions !== configActions) {
291
+ changes.push(`actions: [${existing.actions.join(", ")}] -> [${[...resource.actions].join(", ")}]`);
292
+ }
293
+ const existingScopes = [...(_a = existing.scopes) != null ? _a : []].sort().join(",");
294
+ const configScopes = [...(_b = resource.scopes) != null ? _b : []].sort().join(",");
295
+ if (existingScopes !== configScopes) {
296
+ changes.push("scopes changed");
297
+ }
298
+ if (changes.length > 0) {
299
+ resourceOps.push({ type: "update", key, resource, existing, changes });
300
+ }
301
+ }
302
+ serverResourceByKey.delete(key);
303
+ }
304
+ for (const [key, resource] of serverResourceByKey) {
305
+ if (resource.isActive) {
306
+ resourceOps.push({ type: "archive", key, existing: resource });
307
+ }
308
+ }
309
+ const serverRoleByKey = /* @__PURE__ */ new Map();
310
+ const serverRoleByPreviousKey = /* @__PURE__ */ new Map();
311
+ for (const role of server.roles) {
312
+ if (role.managedBy === "config") {
313
+ serverRoleByKey.set(role.configKey, role);
314
+ }
315
+ for (const prevKey of (_c = role.previousConfigKeys) != null ? _c : []) {
316
+ serverRoleByPreviousKey.set(prevKey, role);
317
+ }
318
+ }
319
+ const renameMap = /* @__PURE__ */ new Map();
320
+ for (const [key, role] of Object.entries(config.roles)) {
321
+ if (role.renamedFrom) {
322
+ renameMap.set(role.renamedFrom, key);
323
+ }
324
+ }
325
+ for (const [key, role] of Object.entries(config.roles)) {
326
+ let existing = serverRoleByKey.get(key);
327
+ if (!existing && role.renamedFrom) {
328
+ existing = (_d = serverRoleByKey.get(role.renamedFrom)) != null ? _d : serverRoleByPreviousKey.get(role.renamedFrom);
329
+ if (existing) {
330
+ const members = (_e = memberCounts[existing.id]) != null ? _e : 0;
331
+ roleOps.push({
332
+ type: "rename",
333
+ key,
334
+ oldKey: role.renamedFrom,
335
+ role,
336
+ existing,
337
+ assignedMembers: members
338
+ });
339
+ serverRoleByKey.delete(existing.configKey);
340
+ serverRoleByKey.delete(key);
341
+ continue;
342
+ }
343
+ }
344
+ if (!existing) {
345
+ roleOps.push({ type: "create", key, role });
346
+ } else {
347
+ const changes = [];
348
+ if (existing.name !== role.name) {
349
+ changes.push(`name: "${existing.name}" -> "${role.name}"`);
350
+ }
351
+ if (((_f = existing.description) != null ? _f : void 0) !== ((_g = role.description) != null ? _g : void 0)) {
352
+ changes.push("description changed");
353
+ }
354
+ const existingGrants = JSON.stringify((_h = existing.grants) != null ? _h : null);
355
+ const configGrants = JSON.stringify(role.grants);
356
+ if (existingGrants !== configGrants) {
357
+ changes.push("grants changed");
358
+ }
359
+ const existingInherits = [...(_i = existing.inherits) != null ? _i : []].sort().join(",");
360
+ const configInherits = [...(_j = role.inherits) != null ? _j : []].sort().join(",");
361
+ if (existingInherits !== configInherits) {
362
+ changes.push("inherits changed");
363
+ }
364
+ if (changes.length > 0) {
365
+ roleOps.push({ type: "update", key, role, existing, changes });
366
+ }
367
+ }
368
+ serverRoleByKey.delete(key);
369
+ }
370
+ for (const [key, role] of serverRoleByKey) {
371
+ if (renameMap.has(key)) continue;
372
+ if (role.isActive) {
373
+ const members = (_k = memberCounts[role.id]) != null ? _k : 0;
374
+ if (members > 0) hasDestructive = true;
375
+ roleOps.push({ type: "archive", key, existing: role, assignedMembers: members });
376
+ }
377
+ }
378
+ const serverConditionByKey = /* @__PURE__ */ new Map();
379
+ for (const cond of server.conditions) {
380
+ serverConditionByKey.set(cond.key, cond);
381
+ }
382
+ if (config.conditions) {
383
+ for (const [key, condFn] of Object.entries(config.conditions)) {
384
+ const existing = serverConditionByKey.get(key);
385
+ const newHash = hashConditionSource(condFn);
386
+ if (!existing) {
387
+ conditionOps.push({ type: "create", key });
388
+ } else {
389
+ if (existing.sourceHash !== newHash) {
390
+ conditionOps.push({ type: "update", key, changes: ["source changed"] });
391
+ }
392
+ }
393
+ serverConditionByKey.delete(key);
394
+ }
395
+ }
396
+ for (const [key, cond] of serverConditionByKey) {
397
+ if (cond.isActive) {
398
+ conditionOps.push({ type: "archive", key, existing: cond });
399
+ }
400
+ }
401
+ return { resourceOps, roleOps, conditionOps, hasDestructive };
402
+ }
403
+
404
+ // src/formatter.ts
405
+ var import_chalk = __toESM(require("chalk"), 1);
406
+ function formatRbacDiff(diff, dryRun) {
407
+ const lines = [];
408
+ if (dryRun) {
409
+ lines.push(
410
+ import_chalk.default.bold("AuthGate RBAC Sync \u2014 DRY RUN") + import_chalk.default.dim(" (use --apply to execute)")
411
+ );
412
+ } else {
413
+ lines.push(import_chalk.default.bold("AuthGate RBAC Sync \u2014 APPLYING CHANGES"));
414
+ }
415
+ lines.push("");
416
+ const totalOps = diff.resourceOps.length + diff.roleOps.length + diff.conditionOps.length;
417
+ if (totalOps === 0) {
418
+ lines.push(import_chalk.default.green(" Everything is in sync. No changes needed."));
419
+ return lines.join("\n");
420
+ }
421
+ if (diff.resourceOps.length > 0) {
422
+ lines.push(import_chalk.default.bold.underline(" Resources"));
423
+ lines.push("");
424
+ for (const op of diff.resourceOps) {
425
+ if (op.type === "create") {
426
+ lines.push(import_chalk.default.green(` + CREATE resource "${op.key}"`));
427
+ lines.push(
428
+ import_chalk.default.dim(` actions: [${[...op.resource.actions].join(", ")}]`)
429
+ );
430
+ if (op.resource.scopes && op.resource.scopes.length > 0) {
431
+ lines.push(
432
+ import_chalk.default.dim(` scopes: [${[...op.resource.scopes].join(", ")}]`)
433
+ );
434
+ }
435
+ } else if (op.type === "update") {
436
+ lines.push(import_chalk.default.yellow(` ~ UPDATE resource "${op.key}"`));
437
+ for (const change of op.changes) {
438
+ lines.push(import_chalk.default.yellow(` ${change}`));
439
+ }
440
+ } else if (op.type === "archive") {
441
+ lines.push(import_chalk.default.red(` - ARCHIVE resource "${op.key}"`));
442
+ }
443
+ }
444
+ lines.push("");
445
+ }
446
+ if (diff.roleOps.length > 0) {
447
+ lines.push(import_chalk.default.bold.underline(" Roles"));
448
+ lines.push("");
449
+ for (const op of diff.roleOps) {
450
+ if (op.type === "create") {
451
+ lines.push(import_chalk.default.green(` + CREATE role "${op.key}"`));
452
+ if (op.role.description) {
453
+ lines.push(import_chalk.default.dim(` ${op.role.description}`));
454
+ }
455
+ if (op.role.inherits && op.role.inherits.length > 0) {
456
+ lines.push(
457
+ import_chalk.default.dim(` inherits: [${[...op.role.inherits].join(", ")}]`)
458
+ );
459
+ }
460
+ } else if (op.type === "update") {
461
+ lines.push(import_chalk.default.yellow(` ~ UPDATE role "${op.key}"`));
462
+ for (const change of op.changes) {
463
+ lines.push(import_chalk.default.yellow(` ${change}`));
464
+ }
465
+ } else if (op.type === "archive") {
466
+ lines.push(import_chalk.default.red(` - ARCHIVE role "${op.key}"`));
467
+ if (op.assignedMembers > 0) {
468
+ lines.push(
469
+ import_chalk.default.red.bold(
470
+ ` WARNING: ${op.assignedMembers} assigned member${op.assignedMembers > 1 ? "s" : ""} will lose this role`
471
+ )
472
+ );
473
+ }
474
+ } else if (op.type === "rename") {
475
+ lines.push(
476
+ import_chalk.default.cyan(` ~ RENAME role "${op.oldKey}" -> "${op.key}"`)
477
+ );
478
+ if (op.assignedMembers > 0) {
479
+ lines.push(
480
+ import_chalk.default.dim(
481
+ ` ${op.assignedMembers} member${op.assignedMembers > 1 ? "s" : ""} preserved`
482
+ )
483
+ );
484
+ }
485
+ }
486
+ }
487
+ lines.push("");
488
+ }
489
+ if (diff.conditionOps.length > 0) {
490
+ lines.push(import_chalk.default.bold.underline(" Conditions"));
491
+ lines.push("");
492
+ for (const op of diff.conditionOps) {
493
+ if (op.type === "create") {
494
+ lines.push(import_chalk.default.green(` + CREATE condition "${op.key}"`));
495
+ } else if (op.type === "update") {
496
+ lines.push(import_chalk.default.yellow(` ~ UPDATE condition "${op.key}"`));
497
+ for (const change of op.changes) {
498
+ lines.push(import_chalk.default.yellow(` ${change}`));
499
+ }
500
+ } else if (op.type === "archive") {
501
+ lines.push(import_chalk.default.red(` - ARCHIVE condition "${op.key}"`));
502
+ }
503
+ }
504
+ lines.push("");
505
+ }
506
+ const allOps = [
507
+ ...diff.resourceOps.map((o) => o.type),
508
+ ...diff.roleOps.map((o) => o.type),
509
+ ...diff.conditionOps.map((o) => o.type)
510
+ ];
511
+ const creates = allOps.filter((t) => t === "create").length;
512
+ const updates = allOps.filter((t) => t === "update").length;
513
+ const renames = allOps.filter((t) => t === "rename").length;
514
+ const archives = allOps.filter((t) => t === "archive").length;
515
+ lines.push(
516
+ import_chalk.default.dim(
517
+ ` Summary: ${creates} create, ${updates} update, ${renames} rename, ${archives} archive.`
518
+ ) + (dryRun ? import_chalk.default.dim(" Run with --apply to execute.") : "")
519
+ );
520
+ if (diff.hasDestructive && dryRun) {
521
+ lines.push(
522
+ import_chalk.default.red.bold(
523
+ "\n WARNING: Destructive changes detected (roles with assigned members)."
524
+ ) + import_chalk.default.red(" Use --apply --force to proceed.")
525
+ );
526
+ }
527
+ return lines.join("\n");
528
+ }
529
+
530
+ // src/sync.ts
531
+ var RbacSyncClient = class {
532
+ constructor(config) {
533
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
534
+ this.apiKey = config.apiKey;
535
+ }
536
+ async request(method, path, body) {
537
+ var _a, _b;
538
+ const url = `${this.baseUrl}${path}`;
539
+ const headers = {
540
+ Authorization: `Bearer ${this.apiKey}`,
541
+ "Content-Type": "application/json"
542
+ };
543
+ const res = await fetch(url, {
544
+ method,
545
+ headers,
546
+ body: body ? JSON.stringify(body) : void 0
547
+ });
548
+ if (!res.ok) {
549
+ const text = await res.text();
550
+ let message;
551
+ try {
552
+ const json = JSON.parse(text);
553
+ message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : text;
554
+ } catch (e) {
555
+ message = text;
556
+ }
557
+ throw new Error(`API error (${res.status}): ${message}`);
558
+ }
559
+ return res.json();
560
+ }
561
+ async getState() {
562
+ return this.request("GET", "/api/v1/rbac/sync/state");
563
+ }
564
+ async getMemberCounts() {
565
+ return this.request(
566
+ "GET",
567
+ "/api/v1/rbac/sync/member-counts"
568
+ );
569
+ }
570
+ async apply(config, force) {
571
+ return this.request("POST", "/api/v1/rbac/sync/apply", {
572
+ resources: config.resources,
573
+ roles: config.roles,
574
+ conditions: config.conditions ? Object.fromEntries(
575
+ Object.entries(config.conditions).map(([k]) => [k, { key: k }])
576
+ ) : void 0,
577
+ force
578
+ });
579
+ }
580
+ };
581
+
582
+ // src/cli.ts
583
+ var import_chalk2 = __toESM(require("chalk"), 1);
584
+ var import_fs2 = require("fs");
585
+ var import_path2 = require("path");
586
+ var HELP = `
587
+ Usage: @auth-gate/rbac <command> [options]
588
+
589
+ Commands:
590
+ sync Preview or apply RBAC config changes
591
+ init Create a starter authgate.rbac.ts config file
592
+ check Validate config locally (no server round-trip)
593
+
594
+ Options (sync):
595
+ --apply Apply changes (default: dry-run only)
596
+ --force Allow archiving roles with assigned members
597
+ --strict Treat warnings as errors (recommended for CI/CD)
598
+ --json Output diff as JSON
599
+
600
+ Environment:
601
+ AUTHGATE_API_KEY Your project API key (required for sync)
602
+ AUTHGATE_BASE_URL AuthGate instance URL (required for sync)
603
+ `;
604
+ async function main() {
605
+ const args = process.argv.slice(2);
606
+ const command = args[0];
607
+ if (!command || command === "--help" || command === "-h") {
608
+ console.log(HELP);
609
+ process.exit(0);
610
+ }
611
+ if (command === "init") {
612
+ await runInit();
613
+ return;
614
+ }
615
+ if (command === "check") {
616
+ await runCheck();
617
+ return;
618
+ }
619
+ if (command === "sync") {
620
+ const apply = args.includes("--apply");
621
+ const force = args.includes("--force");
622
+ const strict = args.includes("--strict");
623
+ const json = args.includes("--json");
624
+ await runSync({ apply, force, strict, json });
625
+ return;
626
+ }
627
+ console.error(import_chalk2.default.red(`Unknown command: ${command}`));
628
+ console.log(HELP);
629
+ process.exit(1);
630
+ }
631
+ async function runInit() {
632
+ const configPath = (0, import_path2.resolve)(process.cwd(), "authgate.rbac.ts");
633
+ if ((0, import_fs2.existsSync)(configPath)) {
634
+ console.error(import_chalk2.default.yellow(`Config file already exists: ${configPath}`));
635
+ process.exit(1);
636
+ }
637
+ const template = `import { defineRbac } from "@auth-gate/rbac";
638
+
639
+ /**
640
+ * RBAC config \u2014 resource/role/permission keys provide full IDE autocomplete.
641
+ *
642
+ * Sync: npx @auth-gate/rbac sync --apply
643
+ */
644
+ export const rbac = defineRbac({
645
+ resources: {
646
+ documents: { actions: ["read", "write", "delete", "share"] },
647
+ billing: { actions: ["read", "manage"] },
648
+ members: { actions: ["invite", "remove", "update_role"] },
649
+ },
650
+ roles: {
651
+ admin: {
652
+ name: "Administrator",
653
+ grants: {
654
+ documents: { read: true, write: true, delete: true, share: true },
655
+ billing: { read: true, manage: true },
656
+ members: { invite: true, remove: true, update_role: true },
657
+ },
658
+ },
659
+ editor: {
660
+ name: "Editor",
661
+ grants: {
662
+ documents: { read: true, write: true },
663
+ billing: { read: true },
664
+ },
665
+ },
666
+ viewer: {
667
+ name: "Viewer",
668
+ grants: {
669
+ documents: { read: true },
670
+ },
671
+ },
672
+ },
673
+ });
674
+
675
+ // Default export for CLI compatibility
676
+ export default rbac;
677
+ `;
678
+ (0, import_fs2.writeFileSync)(configPath, template, "utf-8");
679
+ console.log(import_chalk2.default.green(`Created ${configPath}`));
680
+ console.log(import_chalk2.default.dim("Edit your resources and roles, then run: npx @auth-gate/rbac sync"));
681
+ }
682
+ async function runCheck() {
683
+ let config;
684
+ try {
685
+ config = await loadRbacConfig(process.cwd());
686
+ } catch (err) {
687
+ console.error(import_chalk2.default.red(`Config error: ${err.message}`));
688
+ process.exit(1);
689
+ }
690
+ const resourceCount = Object.keys(config.resources).length;
691
+ const roleCount = Object.keys(config.roles).length;
692
+ let permCount = 0;
693
+ for (const resource of Object.values(config.resources)) {
694
+ permCount += resource.actions.length;
695
+ }
696
+ console.log(import_chalk2.default.green("Config valid!"));
697
+ console.log(import_chalk2.default.dim(`${resourceCount} resource${resourceCount > 1 ? "s" : ""}, ${roleCount} role${roleCount > 1 ? "s" : ""}, ${permCount} permission${permCount > 1 ? "s" : ""}`));
698
+ }
699
+ async function runSync(opts) {
700
+ const apiKey = process.env.AUTHGATE_API_KEY;
701
+ const baseUrl = process.env.AUTHGATE_BASE_URL;
702
+ if (!apiKey) {
703
+ console.error(import_chalk2.default.red("Missing AUTHGATE_API_KEY environment variable."));
704
+ process.exit(1);
705
+ }
706
+ if (!baseUrl) {
707
+ console.error(import_chalk2.default.red("Missing AUTHGATE_BASE_URL environment variable."));
708
+ process.exit(1);
709
+ }
710
+ let config;
711
+ try {
712
+ config = await loadRbacConfig(process.cwd());
713
+ } catch (err) {
714
+ console.error(import_chalk2.default.red(`Config error: ${err.message}`));
715
+ process.exit(1);
716
+ }
717
+ const resourceCount = Object.keys(config.resources).length;
718
+ const roleCount = Object.keys(config.roles).length;
719
+ console.log(import_chalk2.default.dim(`Loaded config: ${resourceCount} resource${resourceCount > 1 ? "s" : ""}, ${roleCount} role${roleCount > 1 ? "s" : ""}`));
720
+ const client = new RbacSyncClient({ baseUrl, apiKey });
721
+ let serverState;
722
+ let memberCounts;
723
+ try {
724
+ [serverState, memberCounts] = await Promise.all([
725
+ client.getState(),
726
+ client.getMemberCounts()
727
+ ]);
728
+ } catch (err) {
729
+ console.error(import_chalk2.default.red(`Failed to fetch server state: ${err.message}`));
730
+ process.exit(1);
731
+ }
732
+ const diff = computeRbacDiff(config, serverState, memberCounts);
733
+ if (opts.json) {
734
+ console.log(JSON.stringify({
735
+ dryRun: !opts.apply,
736
+ resourceOps: diff.resourceOps.map((op) => ({ type: op.type, key: op.key })),
737
+ roleOps: diff.roleOps.map((op) => __spreadValues(__spreadValues({
738
+ type: op.type,
739
+ key: op.key
740
+ }, op.type === "rename" ? { oldKey: op.oldKey } : {}), op.type === "archive" ? { assignedMembers: op.assignedMembers } : {})),
741
+ conditionOps: diff.conditionOps.map((op) => ({ type: op.type, key: op.key })),
742
+ hasDestructive: diff.hasDestructive
743
+ }, null, 2));
744
+ process.exit(diff.hasDestructive && opts.strict ? 1 : 0);
745
+ }
746
+ console.log("");
747
+ console.log(formatRbacDiff(diff, !opts.apply));
748
+ if (!opts.apply) {
749
+ if (opts.strict && diff.hasDestructive) {
750
+ console.error(import_chalk2.default.red.bold("\n --strict: destructive changes detected. Failing."));
751
+ process.exit(1);
752
+ }
753
+ process.exit(0);
754
+ }
755
+ if (diff.hasDestructive && !opts.force) {
756
+ console.error(import_chalk2.default.red("\nDestructive changes require --force flag."));
757
+ process.exit(1);
758
+ }
759
+ const totalOps = diff.resourceOps.length + diff.roleOps.length + diff.conditionOps.length;
760
+ if (totalOps === 0) {
761
+ process.exit(0);
762
+ }
763
+ try {
764
+ console.log("");
765
+ const result = await client.apply(config, opts.force);
766
+ console.log(import_chalk2.default.green.bold("Sync complete!"));
767
+ if (result.created.length) console.log(import_chalk2.default.green(` Created: ${result.created.join(", ")}`));
768
+ if (result.updated.length) console.log(import_chalk2.default.yellow(` Updated: ${result.updated.join(", ")}`));
769
+ if (result.renamed.length) console.log(import_chalk2.default.cyan(` Renamed: ${result.renamed.join(", ")}`));
770
+ if (result.archived.length) console.log(import_chalk2.default.red(` Archived: ${result.archived.join(", ")}`));
771
+ if (result.warnings.length) {
772
+ for (const w of result.warnings) {
773
+ console.log(import_chalk2.default.yellow(` Warning: ${w}`));
774
+ }
775
+ if (opts.strict) {
776
+ console.error(import_chalk2.default.red.bold(`
777
+ --strict: ${result.warnings.length} warning${result.warnings.length > 1 ? "s" : ""} treated as errors.`));
778
+ process.exit(1);
779
+ }
780
+ }
781
+ } catch (err) {
782
+ console.error(import_chalk2.default.red(`Sync failed: ${err.message}`));
783
+ process.exit(1);
784
+ }
785
+ }
786
+ main().catch((err) => {
787
+ console.error(import_chalk2.default.red(err.message));
788
+ process.exit(1);
789
+ });