@auth-gate/rbac 0.8.0 → 0.8.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.
@@ -14,6 +14,12 @@ var __spreadValues = (a, b) => {
14
14
  }
15
15
  return a;
16
16
  };
17
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
18
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
19
+ }) : x)(function(x) {
20
+ if (typeof require !== "undefined") return require.apply(this, arguments);
21
+ throw Error('Dynamic require of "' + x + '" is not supported');
22
+ });
17
23
 
18
24
  // src/validator.ts
19
25
  var KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
@@ -63,15 +69,28 @@ function validateRbacConfig(config) {
63
69
  );
64
70
  }
65
71
  }
66
- resourceActions.set(key, new Set(resource.actions));
72
+ const actionSet = /* @__PURE__ */ new Set();
73
+ for (const action of resource.actions) {
74
+ if (actionSet.has(action)) {
75
+ throw new Error(`Resource "${key}" has duplicate action: "${action}".`);
76
+ }
77
+ actionSet.add(action);
78
+ }
79
+ resourceActions.set(key, actionSet);
67
80
  if (resource.scopes !== void 0) {
68
81
  if (!Array.isArray(resource.scopes)) {
69
82
  throw new Error(`Resource "${key}".scopes must be an array of strings.`);
70
83
  }
84
+ const scopeSet = /* @__PURE__ */ new Set();
71
85
  for (let i = 0; i < resource.scopes.length; i++) {
72
86
  if (typeof resource.scopes[i] !== "string") {
73
87
  throw new Error(`Resource "${key}".scopes[${i}] must be a string.`);
74
88
  }
89
+ const scope = resource.scopes[i];
90
+ if (scopeSet.has(scope)) {
91
+ throw new Error(`Resource "${key}" has duplicate scope: "${scope}".`);
92
+ }
93
+ scopeSet.add(scope);
75
94
  }
76
95
  }
77
96
  }
@@ -112,11 +131,22 @@ function validateRbacConfig(config) {
112
131
  );
113
132
  }
114
133
  const value = actionsGrant[actionKey];
115
- if (value !== true && typeof value !== "string" && !(value && typeof value === "object" && "when" in value && typeof value.when === "string")) {
134
+ if (value !== true && (typeof value !== "string" || value === "") && !(value && typeof value === "object" && "when" in value && typeof value.when === "string" && value.when !== "")) {
116
135
  throw new Error(
117
- `Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a scope string, or { when: string }.`
136
+ `Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a non-empty scope string, or { when: non-empty string }.`
118
137
  );
119
138
  }
139
+ if (typeof value === "string" && value !== "") {
140
+ const resource = resources[resourceKey];
141
+ if (resource.scopes && Array.isArray(resource.scopes)) {
142
+ const declaredScopes = new Set(resource.scopes);
143
+ if (!declaredScopes.has(value)) {
144
+ throw new Error(
145
+ `Role "${key}".grants.${resourceKey}.${actionKey} scope "${value}" is not in declared scopes for "${resourceKey}": ${[...declaredScopes].join(", ")}.`
146
+ );
147
+ }
148
+ }
149
+ }
120
150
  }
121
151
  }
122
152
  }
@@ -185,6 +215,7 @@ function validateRbacConfig(config) {
185
215
  renameTargets.set(role.renamedFrom, key);
186
216
  }
187
217
  }
218
+ const conditionKeys = /* @__PURE__ */ new Set();
188
219
  if (c.conditions !== void 0) {
189
220
  if (typeof c.conditions !== "object" || Array.isArray(c.conditions) || c.conditions === null) {
190
221
  throw new Error("RBAC config `conditions` must be an object mapping names to functions.");
@@ -196,6 +227,23 @@ function validateRbacConfig(config) {
196
227
  `Condition "${condKey}" must be a function. Got ${typeof condValue}.`
197
228
  );
198
229
  }
230
+ conditionKeys.add(condKey);
231
+ }
232
+ }
233
+ for (const key of roleKeys) {
234
+ const role = roles[key];
235
+ const grants = role.grants;
236
+ for (const [resourceKey, actionGrants] of Object.entries(grants)) {
237
+ for (const [actionKey, value] of Object.entries(actionGrants)) {
238
+ if (value && typeof value === "object" && "when" in value) {
239
+ const whenKey = value.when;
240
+ if (!conditionKeys.has(whenKey)) {
241
+ throw new Error(
242
+ `Role "${key}".grants.${resourceKey}.${actionKey} references condition "${whenKey}" but it is not declared in conditions.` + (conditionKeys.size > 0 ? ` Declared conditions: ${[...conditionKeys].join(", ")}.` : ` No conditions are declared.`)
243
+ );
244
+ }
245
+ }
246
+ }
199
247
  }
200
248
  }
201
249
  return config;
@@ -234,17 +282,27 @@ Run \`npx @auth-gate/rbac init\` to create one.`
234
282
  }
235
283
 
236
284
  // src/diff.ts
285
+ function deepEqual(a, b) {
286
+ if (a === b) return true;
287
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
288
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
289
+ if (Array.isArray(a) && Array.isArray(b)) {
290
+ if (a.length !== b.length) return false;
291
+ return a.every((val, i) => deepEqual(val, b[i]));
292
+ }
293
+ const keysA = Object.keys(a).sort();
294
+ const keysB = Object.keys(b).sort();
295
+ if (keysA.length !== keysB.length) return false;
296
+ return keysA.every(
297
+ (key, i) => key === keysB[i] && deepEqual(a[key], b[key])
298
+ );
299
+ }
237
300
  function hashConditionSource(fn) {
238
- const src = fn.toString();
239
- let hash = 0;
240
- for (let i = 0; i < src.length; i++) {
241
- const ch = src.charCodeAt(i);
242
- hash = (hash << 5) - hash + ch | 0;
243
- }
244
- return hash.toString(36);
301
+ const { createHash } = __require("crypto");
302
+ return createHash("sha256").update(fn.toString()).digest("hex").slice(0, 16);
245
303
  }
246
304
  function computeRbacDiff(config, server, memberCounts) {
247
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
305
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
248
306
  const resourceOps = [];
249
307
  const roleOps = [];
250
308
  const conditionOps = [];
@@ -303,7 +361,7 @@ function computeRbacDiff(config, server, memberCounts) {
303
361
  if (!existing && role.renamedFrom) {
304
362
  existing = (_d = serverRoleByKey.get(role.renamedFrom)) != null ? _d : serverRoleByPreviousKey.get(role.renamedFrom);
305
363
  if (existing) {
306
- const members = (_e = memberCounts[existing.id]) != null ? _e : 0;
364
+ const members = (_e = memberCounts[existing.configKey]) != null ? _e : 0;
307
365
  roleOps.push({
308
366
  type: "rename",
309
367
  key,
@@ -327,13 +385,11 @@ function computeRbacDiff(config, server, memberCounts) {
327
385
  if (((_f = existing.description) != null ? _f : void 0) !== ((_g = role.description) != null ? _g : void 0)) {
328
386
  changes.push("description changed");
329
387
  }
330
- const existingGrants = JSON.stringify((_h = existing.grants) != null ? _h : null);
331
- const configGrants = JSON.stringify(role.grants);
332
- if (existingGrants !== configGrants) {
388
+ if (!deepEqual((_h = existing.grants) != null ? _h : null, (_i = role.grants) != null ? _i : null)) {
333
389
  changes.push("grants changed");
334
390
  }
335
- const existingInherits = [...(_i = existing.inherits) != null ? _i : []].sort().join(",");
336
- const configInherits = [...(_j = role.inherits) != null ? _j : []].sort().join(",");
391
+ const existingInherits = [...(_j = existing.inherits) != null ? _j : []].sort().join(",");
392
+ const configInherits = [...(_k = role.inherits) != null ? _k : []].sort().join(",");
337
393
  if (existingInherits !== configInherits) {
338
394
  changes.push("inherits changed");
339
395
  }
@@ -346,7 +402,7 @@ function computeRbacDiff(config, server, memberCounts) {
346
402
  for (const [key, role] of serverRoleByKey) {
347
403
  if (renameMap.has(key)) continue;
348
404
  if (role.isActive) {
349
- const members = (_k = memberCounts[role.id]) != null ? _k : 0;
405
+ const members = (_l = memberCounts[role.configKey]) != null ? _l : 0;
350
406
  if (members > 0) hasDestructive = true;
351
407
  roleOps.push({ type: "archive", key, existing: role, assignedMembers: members });
352
408
  }
@@ -508,6 +564,11 @@ var RbacSyncClient = class {
508
564
  constructor(config) {
509
565
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
510
566
  this.apiKey = config.apiKey;
567
+ if (!this.baseUrl.startsWith("https://") && !this.baseUrl.startsWith("http://localhost") && !this.baseUrl.startsWith("http://127.0.0.1")) {
568
+ console.warn(
569
+ "WARNING: AUTHGATE_BASE_URL does not use HTTPS. API key may be transmitted insecurely."
570
+ );
571
+ }
511
572
  }
512
573
  async request(method, path, body) {
513
574
  var _a, _b;
@@ -526,9 +587,12 @@ var RbacSyncClient = class {
526
587
  let message;
527
588
  try {
528
589
  const json = JSON.parse(text);
529
- message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : text;
590
+ message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : "Unknown error";
530
591
  } catch (e) {
531
- message = text;
592
+ message = text.length > 500 ? text.slice(0, 500) + "..." : text;
593
+ }
594
+ if (this.apiKey) {
595
+ message = message.replaceAll(this.apiKey, "[REDACTED]");
532
596
  }
533
597
  throw new Error(`API error (${res.status}): ${message}`);
534
598
  }
@@ -548,7 +612,10 @@ var RbacSyncClient = class {
548
612
  resources: config.resources,
549
613
  roles: config.roles,
550
614
  conditions: config.conditions ? Object.fromEntries(
551
- Object.entries(config.conditions).map(([k]) => [k, { key: k }])
615
+ Object.entries(config.conditions).map(([k, fn]) => [
616
+ k,
617
+ { key: k, sourceHash: hashConditionSource(fn) }
618
+ ])
552
619
  ) : void 0,
553
620
  force
554
621
  });
package/dist/cli.cjs CHANGED
@@ -89,15 +89,28 @@ function validateRbacConfig(config) {
89
89
  );
90
90
  }
91
91
  }
92
- resourceActions.set(key, new Set(resource.actions));
92
+ const actionSet = /* @__PURE__ */ new Set();
93
+ for (const action of resource.actions) {
94
+ if (actionSet.has(action)) {
95
+ throw new Error(`Resource "${key}" has duplicate action: "${action}".`);
96
+ }
97
+ actionSet.add(action);
98
+ }
99
+ resourceActions.set(key, actionSet);
93
100
  if (resource.scopes !== void 0) {
94
101
  if (!Array.isArray(resource.scopes)) {
95
102
  throw new Error(`Resource "${key}".scopes must be an array of strings.`);
96
103
  }
104
+ const scopeSet = /* @__PURE__ */ new Set();
97
105
  for (let i = 0; i < resource.scopes.length; i++) {
98
106
  if (typeof resource.scopes[i] !== "string") {
99
107
  throw new Error(`Resource "${key}".scopes[${i}] must be a string.`);
100
108
  }
109
+ const scope = resource.scopes[i];
110
+ if (scopeSet.has(scope)) {
111
+ throw new Error(`Resource "${key}" has duplicate scope: "${scope}".`);
112
+ }
113
+ scopeSet.add(scope);
101
114
  }
102
115
  }
103
116
  }
@@ -138,11 +151,22 @@ function validateRbacConfig(config) {
138
151
  );
139
152
  }
140
153
  const value = actionsGrant[actionKey];
141
- if (value !== true && typeof value !== "string" && !(value && typeof value === "object" && "when" in value && typeof value.when === "string")) {
154
+ if (value !== true && (typeof value !== "string" || value === "") && !(value && typeof value === "object" && "when" in value && typeof value.when === "string" && value.when !== "")) {
142
155
  throw new Error(
143
- `Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a scope string, or { when: string }.`
156
+ `Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a non-empty scope string, or { when: non-empty string }.`
144
157
  );
145
158
  }
159
+ if (typeof value === "string" && value !== "") {
160
+ const resource = resources[resourceKey];
161
+ if (resource.scopes && Array.isArray(resource.scopes)) {
162
+ const declaredScopes = new Set(resource.scopes);
163
+ if (!declaredScopes.has(value)) {
164
+ throw new Error(
165
+ `Role "${key}".grants.${resourceKey}.${actionKey} scope "${value}" is not in declared scopes for "${resourceKey}": ${[...declaredScopes].join(", ")}.`
166
+ );
167
+ }
168
+ }
169
+ }
146
170
  }
147
171
  }
148
172
  }
@@ -211,6 +235,7 @@ function validateRbacConfig(config) {
211
235
  renameTargets.set(role.renamedFrom, key);
212
236
  }
213
237
  }
238
+ const conditionKeys = /* @__PURE__ */ new Set();
214
239
  if (c.conditions !== void 0) {
215
240
  if (typeof c.conditions !== "object" || Array.isArray(c.conditions) || c.conditions === null) {
216
241
  throw new Error("RBAC config `conditions` must be an object mapping names to functions.");
@@ -222,6 +247,23 @@ function validateRbacConfig(config) {
222
247
  `Condition "${condKey}" must be a function. Got ${typeof condValue}.`
223
248
  );
224
249
  }
250
+ conditionKeys.add(condKey);
251
+ }
252
+ }
253
+ for (const key of roleKeys) {
254
+ const role = roles[key];
255
+ const grants = role.grants;
256
+ for (const [resourceKey, actionGrants] of Object.entries(grants)) {
257
+ for (const [actionKey, value] of Object.entries(actionGrants)) {
258
+ if (value && typeof value === "object" && "when" in value) {
259
+ const whenKey = value.when;
260
+ if (!conditionKeys.has(whenKey)) {
261
+ throw new Error(
262
+ `Role "${key}".grants.${resourceKey}.${actionKey} references condition "${whenKey}" but it is not declared in conditions.` + (conditionKeys.size > 0 ? ` Declared conditions: ${[...conditionKeys].join(", ")}.` : ` No conditions are declared.`)
263
+ );
264
+ }
265
+ }
266
+ }
225
267
  }
226
268
  }
227
269
  return config;
@@ -258,17 +300,27 @@ Run \`npx @auth-gate/rbac init\` to create one.`
258
300
  }
259
301
 
260
302
  // src/diff.ts
303
+ function deepEqual(a, b) {
304
+ if (a === b) return true;
305
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
306
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
307
+ if (Array.isArray(a) && Array.isArray(b)) {
308
+ if (a.length !== b.length) return false;
309
+ return a.every((val, i) => deepEqual(val, b[i]));
310
+ }
311
+ const keysA = Object.keys(a).sort();
312
+ const keysB = Object.keys(b).sort();
313
+ if (keysA.length !== keysB.length) return false;
314
+ return keysA.every(
315
+ (key, i) => key === keysB[i] && deepEqual(a[key], b[key])
316
+ );
317
+ }
261
318
  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);
319
+ const { createHash } = require("crypto");
320
+ return createHash("sha256").update(fn.toString()).digest("hex").slice(0, 16);
269
321
  }
270
322
  function computeRbacDiff(config, server, memberCounts) {
271
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
323
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
272
324
  const resourceOps = [];
273
325
  const roleOps = [];
274
326
  const conditionOps = [];
@@ -327,7 +379,7 @@ function computeRbacDiff(config, server, memberCounts) {
327
379
  if (!existing && role.renamedFrom) {
328
380
  existing = (_d = serverRoleByKey.get(role.renamedFrom)) != null ? _d : serverRoleByPreviousKey.get(role.renamedFrom);
329
381
  if (existing) {
330
- const members = (_e = memberCounts[existing.id]) != null ? _e : 0;
382
+ const members = (_e = memberCounts[existing.configKey]) != null ? _e : 0;
331
383
  roleOps.push({
332
384
  type: "rename",
333
385
  key,
@@ -351,13 +403,11 @@ function computeRbacDiff(config, server, memberCounts) {
351
403
  if (((_f = existing.description) != null ? _f : void 0) !== ((_g = role.description) != null ? _g : void 0)) {
352
404
  changes.push("description changed");
353
405
  }
354
- const existingGrants = JSON.stringify((_h = existing.grants) != null ? _h : null);
355
- const configGrants = JSON.stringify(role.grants);
356
- if (existingGrants !== configGrants) {
406
+ if (!deepEqual((_h = existing.grants) != null ? _h : null, (_i = role.grants) != null ? _i : null)) {
357
407
  changes.push("grants changed");
358
408
  }
359
- const existingInherits = [...(_i = existing.inherits) != null ? _i : []].sort().join(",");
360
- const configInherits = [...(_j = role.inherits) != null ? _j : []].sort().join(",");
409
+ const existingInherits = [...(_j = existing.inherits) != null ? _j : []].sort().join(",");
410
+ const configInherits = [...(_k = role.inherits) != null ? _k : []].sort().join(",");
361
411
  if (existingInherits !== configInherits) {
362
412
  changes.push("inherits changed");
363
413
  }
@@ -370,7 +420,7 @@ function computeRbacDiff(config, server, memberCounts) {
370
420
  for (const [key, role] of serverRoleByKey) {
371
421
  if (renameMap.has(key)) continue;
372
422
  if (role.isActive) {
373
- const members = (_k = memberCounts[role.id]) != null ? _k : 0;
423
+ const members = (_l = memberCounts[role.configKey]) != null ? _l : 0;
374
424
  if (members > 0) hasDestructive = true;
375
425
  roleOps.push({ type: "archive", key, existing: role, assignedMembers: members });
376
426
  }
@@ -532,6 +582,11 @@ var RbacSyncClient = class {
532
582
  constructor(config) {
533
583
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
534
584
  this.apiKey = config.apiKey;
585
+ if (!this.baseUrl.startsWith("https://") && !this.baseUrl.startsWith("http://localhost") && !this.baseUrl.startsWith("http://127.0.0.1")) {
586
+ console.warn(
587
+ "WARNING: AUTHGATE_BASE_URL does not use HTTPS. API key may be transmitted insecurely."
588
+ );
589
+ }
535
590
  }
536
591
  async request(method, path, body) {
537
592
  var _a, _b;
@@ -550,9 +605,12 @@ var RbacSyncClient = class {
550
605
  let message;
551
606
  try {
552
607
  const json = JSON.parse(text);
553
- message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : text;
608
+ message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : "Unknown error";
554
609
  } catch (e) {
555
- message = text;
610
+ message = text.length > 500 ? text.slice(0, 500) + "..." : text;
611
+ }
612
+ if (this.apiKey) {
613
+ message = message.replaceAll(this.apiKey, "[REDACTED]");
556
614
  }
557
615
  throw new Error(`API error (${res.status}): ${message}`);
558
616
  }
@@ -572,7 +630,10 @@ var RbacSyncClient = class {
572
630
  resources: config.resources,
573
631
  roles: config.roles,
574
632
  conditions: config.conditions ? Object.fromEntries(
575
- Object.entries(config.conditions).map(([k]) => [k, { key: k }])
633
+ Object.entries(config.conditions).map(([k, fn]) => [
634
+ k,
635
+ { key: k, sourceHash: hashConditionSource(fn) }
636
+ ])
576
637
  ) : void 0,
577
638
  force
578
639
  });
package/dist/cli.mjs CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  computeRbacDiff,
6
6
  formatRbacDiff,
7
7
  loadRbacConfig
8
- } from "./chunk-HE57TIQI.mjs";
8
+ } from "./chunk-5YEMXO7B.mjs";
9
9
 
10
10
  // src/cli.ts
11
11
  import chalk from "chalk";
package/dist/index.cjs CHANGED
@@ -88,15 +88,28 @@ function validateRbacConfig(config) {
88
88
  );
89
89
  }
90
90
  }
91
- resourceActions.set(key, new Set(resource.actions));
91
+ const actionSet = /* @__PURE__ */ new Set();
92
+ for (const action of resource.actions) {
93
+ if (actionSet.has(action)) {
94
+ throw new Error(`Resource "${key}" has duplicate action: "${action}".`);
95
+ }
96
+ actionSet.add(action);
97
+ }
98
+ resourceActions.set(key, actionSet);
92
99
  if (resource.scopes !== void 0) {
93
100
  if (!Array.isArray(resource.scopes)) {
94
101
  throw new Error(`Resource "${key}".scopes must be an array of strings.`);
95
102
  }
103
+ const scopeSet = /* @__PURE__ */ new Set();
96
104
  for (let i = 0; i < resource.scopes.length; i++) {
97
105
  if (typeof resource.scopes[i] !== "string") {
98
106
  throw new Error(`Resource "${key}".scopes[${i}] must be a string.`);
99
107
  }
108
+ const scope = resource.scopes[i];
109
+ if (scopeSet.has(scope)) {
110
+ throw new Error(`Resource "${key}" has duplicate scope: "${scope}".`);
111
+ }
112
+ scopeSet.add(scope);
100
113
  }
101
114
  }
102
115
  }
@@ -137,11 +150,22 @@ function validateRbacConfig(config) {
137
150
  );
138
151
  }
139
152
  const value = actionsGrant[actionKey];
140
- if (value !== true && typeof value !== "string" && !(value && typeof value === "object" && "when" in value && typeof value.when === "string")) {
153
+ if (value !== true && (typeof value !== "string" || value === "") && !(value && typeof value === "object" && "when" in value && typeof value.when === "string" && value.when !== "")) {
141
154
  throw new Error(
142
- `Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a scope string, or { when: string }.`
155
+ `Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a non-empty scope string, or { when: non-empty string }.`
143
156
  );
144
157
  }
158
+ if (typeof value === "string" && value !== "") {
159
+ const resource = resources[resourceKey];
160
+ if (resource.scopes && Array.isArray(resource.scopes)) {
161
+ const declaredScopes = new Set(resource.scopes);
162
+ if (!declaredScopes.has(value)) {
163
+ throw new Error(
164
+ `Role "${key}".grants.${resourceKey}.${actionKey} scope "${value}" is not in declared scopes for "${resourceKey}": ${[...declaredScopes].join(", ")}.`
165
+ );
166
+ }
167
+ }
168
+ }
145
169
  }
146
170
  }
147
171
  }
@@ -210,6 +234,7 @@ function validateRbacConfig(config) {
210
234
  renameTargets.set(role.renamedFrom, key);
211
235
  }
212
236
  }
237
+ const conditionKeys = /* @__PURE__ */ new Set();
213
238
  if (c.conditions !== void 0) {
214
239
  if (typeof c.conditions !== "object" || Array.isArray(c.conditions) || c.conditions === null) {
215
240
  throw new Error("RBAC config `conditions` must be an object mapping names to functions.");
@@ -221,6 +246,23 @@ function validateRbacConfig(config) {
221
246
  `Condition "${condKey}" must be a function. Got ${typeof condValue}.`
222
247
  );
223
248
  }
249
+ conditionKeys.add(condKey);
250
+ }
251
+ }
252
+ for (const key of roleKeys) {
253
+ const role = roles[key];
254
+ const grants = role.grants;
255
+ for (const [resourceKey, actionGrants] of Object.entries(grants)) {
256
+ for (const [actionKey, value] of Object.entries(actionGrants)) {
257
+ if (value && typeof value === "object" && "when" in value) {
258
+ const whenKey = value.when;
259
+ if (!conditionKeys.has(whenKey)) {
260
+ throw new Error(
261
+ `Role "${key}".grants.${resourceKey}.${actionKey} references condition "${whenKey}" but it is not declared in conditions.` + (conditionKeys.size > 0 ? ` Declared conditions: ${[...conditionKeys].join(", ")}.` : ` No conditions are declared.`)
262
+ );
263
+ }
264
+ }
265
+ }
224
266
  }
225
267
  }
226
268
  return config;
@@ -259,17 +301,27 @@ Run \`npx @auth-gate/rbac init\` to create one.`
259
301
  }
260
302
 
261
303
  // src/diff.ts
304
+ function deepEqual(a, b) {
305
+ if (a === b) return true;
306
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
307
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
308
+ if (Array.isArray(a) && Array.isArray(b)) {
309
+ if (a.length !== b.length) return false;
310
+ return a.every((val, i) => deepEqual(val, b[i]));
311
+ }
312
+ const keysA = Object.keys(a).sort();
313
+ const keysB = Object.keys(b).sort();
314
+ if (keysA.length !== keysB.length) return false;
315
+ return keysA.every(
316
+ (key, i) => key === keysB[i] && deepEqual(a[key], b[key])
317
+ );
318
+ }
262
319
  function hashConditionSource(fn) {
263
- const src = fn.toString();
264
- let hash = 0;
265
- for (let i = 0; i < src.length; i++) {
266
- const ch = src.charCodeAt(i);
267
- hash = (hash << 5) - hash + ch | 0;
268
- }
269
- return hash.toString(36);
320
+ const { createHash } = require("crypto");
321
+ return createHash("sha256").update(fn.toString()).digest("hex").slice(0, 16);
270
322
  }
271
323
  function computeRbacDiff(config, server, memberCounts) {
272
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
324
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
273
325
  const resourceOps = [];
274
326
  const roleOps = [];
275
327
  const conditionOps = [];
@@ -328,7 +380,7 @@ function computeRbacDiff(config, server, memberCounts) {
328
380
  if (!existing && role.renamedFrom) {
329
381
  existing = (_d = serverRoleByKey.get(role.renamedFrom)) != null ? _d : serverRoleByPreviousKey.get(role.renamedFrom);
330
382
  if (existing) {
331
- const members = (_e = memberCounts[existing.id]) != null ? _e : 0;
383
+ const members = (_e = memberCounts[existing.configKey]) != null ? _e : 0;
332
384
  roleOps.push({
333
385
  type: "rename",
334
386
  key,
@@ -352,13 +404,11 @@ function computeRbacDiff(config, server, memberCounts) {
352
404
  if (((_f = existing.description) != null ? _f : void 0) !== ((_g = role.description) != null ? _g : void 0)) {
353
405
  changes.push("description changed");
354
406
  }
355
- const existingGrants = JSON.stringify((_h = existing.grants) != null ? _h : null);
356
- const configGrants = JSON.stringify(role.grants);
357
- if (existingGrants !== configGrants) {
407
+ if (!deepEqual((_h = existing.grants) != null ? _h : null, (_i = role.grants) != null ? _i : null)) {
358
408
  changes.push("grants changed");
359
409
  }
360
- const existingInherits = [...(_i = existing.inherits) != null ? _i : []].sort().join(",");
361
- const configInherits = [...(_j = role.inherits) != null ? _j : []].sort().join(",");
410
+ const existingInherits = [...(_j = existing.inherits) != null ? _j : []].sort().join(",");
411
+ const configInherits = [...(_k = role.inherits) != null ? _k : []].sort().join(",");
362
412
  if (existingInherits !== configInherits) {
363
413
  changes.push("inherits changed");
364
414
  }
@@ -371,7 +421,7 @@ function computeRbacDiff(config, server, memberCounts) {
371
421
  for (const [key, role] of serverRoleByKey) {
372
422
  if (renameMap.has(key)) continue;
373
423
  if (role.isActive) {
374
- const members = (_k = memberCounts[role.id]) != null ? _k : 0;
424
+ const members = (_l = memberCounts[role.configKey]) != null ? _l : 0;
375
425
  if (members > 0) hasDestructive = true;
376
426
  roleOps.push({ type: "archive", key, existing: role, assignedMembers: members });
377
427
  }
@@ -407,6 +457,11 @@ var RbacSyncClient = class {
407
457
  constructor(config) {
408
458
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
409
459
  this.apiKey = config.apiKey;
460
+ if (!this.baseUrl.startsWith("https://") && !this.baseUrl.startsWith("http://localhost") && !this.baseUrl.startsWith("http://127.0.0.1")) {
461
+ console.warn(
462
+ "WARNING: AUTHGATE_BASE_URL does not use HTTPS. API key may be transmitted insecurely."
463
+ );
464
+ }
410
465
  }
411
466
  async request(method, path, body) {
412
467
  var _a, _b;
@@ -425,9 +480,12 @@ var RbacSyncClient = class {
425
480
  let message;
426
481
  try {
427
482
  const json = JSON.parse(text);
428
- message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : text;
483
+ message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : "Unknown error";
429
484
  } catch (e) {
430
- message = text;
485
+ message = text.length > 500 ? text.slice(0, 500) + "..." : text;
486
+ }
487
+ if (this.apiKey) {
488
+ message = message.replaceAll(this.apiKey, "[REDACTED]");
431
489
  }
432
490
  throw new Error(`API error (${res.status}): ${message}`);
433
491
  }
@@ -447,7 +505,10 @@ var RbacSyncClient = class {
447
505
  resources: config.resources,
448
506
  roles: config.roles,
449
507
  conditions: config.conditions ? Object.fromEntries(
450
- Object.entries(config.conditions).map(([k]) => [k, { key: k }])
508
+ Object.entries(config.conditions).map(([k, fn]) => [
509
+ k,
510
+ { key: k, sourceHash: hashConditionSource(fn) }
511
+ ])
451
512
  ) : void 0,
452
513
  force
453
514
  });
@@ -581,7 +642,10 @@ function formatRbacDiff(diff, dryRun) {
581
642
  }
582
643
 
583
644
  // src/index.ts
584
- function defineRbac(config) {
645
+ function defineRbac(config, opts) {
646
+ if ((opts == null ? void 0 : opts.validate) !== false) {
647
+ validateRbacConfig(config);
648
+ }
585
649
  const resources = {};
586
650
  for (const [key, resource] of Object.entries(config.resources)) {
587
651
  const actions = {};
package/dist/index.d.cts CHANGED
@@ -245,7 +245,7 @@ interface DiffResult {
245
245
  conditionOps: ConditionOp[];
246
246
  hasDestructive: boolean;
247
247
  }
248
- /** Simple hash of a condition function's source code for change detection. */
248
+ /** Hash of a condition function's source code for change detection. */
249
249
  declare function hashConditionSource(fn: Function): string;
250
250
  declare function computeRbacDiff(config: RbacConfig, server: ServerState, memberCounts: Record<string, number>): DiffResult;
251
251
 
@@ -310,6 +310,8 @@ declare function formatRbacDiff(diff: DiffResult, dryRun: boolean): string;
310
310
  * rbac.permissions.documents.write // "documents:write"
311
311
  * ```
312
312
  */
313
- declare function defineRbac<const T extends RbacConfig>(config: T): TypedRbac<T>;
313
+ declare function defineRbac<const T extends RbacConfig>(config: T, opts?: {
314
+ validate?: boolean;
315
+ }): TypedRbac<T>;
314
316
 
315
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 };
package/dist/index.d.ts CHANGED
@@ -245,7 +245,7 @@ interface DiffResult {
245
245
  conditionOps: ConditionOp[];
246
246
  hasDestructive: boolean;
247
247
  }
248
- /** Simple hash of a condition function's source code for change detection. */
248
+ /** Hash of a condition function's source code for change detection. */
249
249
  declare function hashConditionSource(fn: Function): string;
250
250
  declare function computeRbacDiff(config: RbacConfig, server: ServerState, memberCounts: Record<string, number>): DiffResult;
251
251
 
@@ -310,6 +310,8 @@ declare function formatRbacDiff(diff: DiffResult, dryRun: boolean): string;
310
310
  * rbac.permissions.documents.write // "documents:write"
311
311
  * ```
312
312
  */
313
- declare function defineRbac<const T extends RbacConfig>(config: T): TypedRbac<T>;
313
+ declare function defineRbac<const T extends RbacConfig>(config: T, opts?: {
314
+ validate?: boolean;
315
+ }): TypedRbac<T>;
314
316
 
315
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 };
package/dist/index.mjs CHANGED
@@ -5,10 +5,13 @@ import {
5
5
  hashConditionSource,
6
6
  loadRbacConfig,
7
7
  validateRbacConfig
8
- } from "./chunk-HE57TIQI.mjs";
8
+ } from "./chunk-5YEMXO7B.mjs";
9
9
 
10
10
  // src/index.ts
11
- function defineRbac(config) {
11
+ function defineRbac(config, opts) {
12
+ if ((opts == null ? void 0 : opts.validate) !== false) {
13
+ validateRbacConfig(config);
14
+ }
12
15
  const resources = {};
13
16
  for (const [key, resource] of Object.entries(config.resources)) {
14
17
  const actions = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auth-gate/rbac",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {