@auth-gate/rbac 0.8.0 → 0.9.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/{chunk-HE57TIQI.mjs → chunk-ZFKXT2MP.mjs} +111 -21
- package/dist/cli.cjs +106 -21
- package/dist/cli.mjs +2 -1
- package/dist/index.cjs +199 -23
- package/dist/index.d.cts +124 -8
- package/dist/index.d.ts +124 -8
- package/dist/index.mjs +94 -3
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -32,6 +32,7 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
RbacSyncClient: () => RbacSyncClient,
|
|
34
34
|
computeRbacDiff: () => computeRbacDiff,
|
|
35
|
+
createRoleManagement: () => createRoleManagement,
|
|
35
36
|
defineRbac: () => defineRbac,
|
|
36
37
|
formatRbacDiff: () => formatRbacDiff,
|
|
37
38
|
hashConditionSource: () => hashConditionSource,
|
|
@@ -88,15 +89,28 @@ function validateRbacConfig(config) {
|
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
|
-
|
|
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);
|
|
92
100
|
if (resource.scopes !== void 0) {
|
|
93
101
|
if (!Array.isArray(resource.scopes)) {
|
|
94
102
|
throw new Error(`Resource "${key}".scopes must be an array of strings.`);
|
|
95
103
|
}
|
|
104
|
+
const scopeSet = /* @__PURE__ */ new Set();
|
|
96
105
|
for (let i = 0; i < resource.scopes.length; i++) {
|
|
97
106
|
if (typeof resource.scopes[i] !== "string") {
|
|
98
107
|
throw new Error(`Resource "${key}".scopes[${i}] must be a string.`);
|
|
99
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);
|
|
100
114
|
}
|
|
101
115
|
}
|
|
102
116
|
}
|
|
@@ -137,14 +151,40 @@ function validateRbacConfig(config) {
|
|
|
137
151
|
);
|
|
138
152
|
}
|
|
139
153
|
const value = actionsGrant[actionKey];
|
|
140
|
-
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 !== "")) {
|
|
141
155
|
throw new Error(
|
|
142
|
-
`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 }.`
|
|
143
157
|
);
|
|
144
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
|
+
}
|
|
145
170
|
}
|
|
146
171
|
}
|
|
147
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
|
+
}
|
|
148
188
|
for (const key of roleKeys) {
|
|
149
189
|
const role = roles[key];
|
|
150
190
|
if (role.inherits !== void 0) {
|
|
@@ -210,6 +250,7 @@ function validateRbacConfig(config) {
|
|
|
210
250
|
renameTargets.set(role.renamedFrom, key);
|
|
211
251
|
}
|
|
212
252
|
}
|
|
253
|
+
const conditionKeys = /* @__PURE__ */ new Set();
|
|
213
254
|
if (c.conditions !== void 0) {
|
|
214
255
|
if (typeof c.conditions !== "object" || Array.isArray(c.conditions) || c.conditions === null) {
|
|
215
256
|
throw new Error("RBAC config `conditions` must be an object mapping names to functions.");
|
|
@@ -221,6 +262,23 @@ function validateRbacConfig(config) {
|
|
|
221
262
|
`Condition "${condKey}" must be a function. Got ${typeof condValue}.`
|
|
222
263
|
);
|
|
223
264
|
}
|
|
265
|
+
conditionKeys.add(condKey);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const key of roleKeys) {
|
|
269
|
+
const role = roles[key];
|
|
270
|
+
const grants = role.grants;
|
|
271
|
+
for (const [resourceKey, actionGrants] of Object.entries(grants)) {
|
|
272
|
+
for (const [actionKey, value] of Object.entries(actionGrants)) {
|
|
273
|
+
if (value && typeof value === "object" && "when" in value) {
|
|
274
|
+
const whenKey = value.when;
|
|
275
|
+
if (!conditionKeys.has(whenKey)) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`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.`)
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
224
282
|
}
|
|
225
283
|
}
|
|
226
284
|
return config;
|
|
@@ -259,17 +317,27 @@ Run \`npx @auth-gate/rbac init\` to create one.`
|
|
|
259
317
|
}
|
|
260
318
|
|
|
261
319
|
// src/diff.ts
|
|
320
|
+
function deepEqual(a, b) {
|
|
321
|
+
if (a === b) return true;
|
|
322
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
323
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
324
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
325
|
+
if (a.length !== b.length) return false;
|
|
326
|
+
return a.every((val, i) => deepEqual(val, b[i]));
|
|
327
|
+
}
|
|
328
|
+
const keysA = Object.keys(a).sort();
|
|
329
|
+
const keysB = Object.keys(b).sort();
|
|
330
|
+
if (keysA.length !== keysB.length) return false;
|
|
331
|
+
return keysA.every(
|
|
332
|
+
(key, i) => key === keysB[i] && deepEqual(a[key], b[key])
|
|
333
|
+
);
|
|
334
|
+
}
|
|
262
335
|
function hashConditionSource(fn) {
|
|
263
|
-
const
|
|
264
|
-
|
|
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);
|
|
336
|
+
const { createHash } = require("crypto");
|
|
337
|
+
return createHash("sha256").update(fn.toString()).digest("hex").slice(0, 16);
|
|
270
338
|
}
|
|
271
339
|
function computeRbacDiff(config, server, memberCounts) {
|
|
272
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
340
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
|
|
273
341
|
const resourceOps = [];
|
|
274
342
|
const roleOps = [];
|
|
275
343
|
const conditionOps = [];
|
|
@@ -328,7 +396,7 @@ function computeRbacDiff(config, server, memberCounts) {
|
|
|
328
396
|
if (!existing && role.renamedFrom) {
|
|
329
397
|
existing = (_d = serverRoleByKey.get(role.renamedFrom)) != null ? _d : serverRoleByPreviousKey.get(role.renamedFrom);
|
|
330
398
|
if (existing) {
|
|
331
|
-
const members = (_e = memberCounts[existing.
|
|
399
|
+
const members = (_e = memberCounts[existing.configKey]) != null ? _e : 0;
|
|
332
400
|
roleOps.push({
|
|
333
401
|
type: "rename",
|
|
334
402
|
key,
|
|
@@ -352,16 +420,19 @@ function computeRbacDiff(config, server, memberCounts) {
|
|
|
352
420
|
if (((_f = existing.description) != null ? _f : void 0) !== ((_g = role.description) != null ? _g : void 0)) {
|
|
353
421
|
changes.push("description changed");
|
|
354
422
|
}
|
|
355
|
-
|
|
356
|
-
const configGrants = JSON.stringify(role.grants);
|
|
357
|
-
if (existingGrants !== configGrants) {
|
|
423
|
+
if (!deepEqual((_h = existing.grants) != null ? _h : null, (_i = role.grants) != null ? _i : null)) {
|
|
358
424
|
changes.push("grants changed");
|
|
359
425
|
}
|
|
360
|
-
const existingInherits = [...(
|
|
361
|
-
const configInherits = [...(
|
|
426
|
+
const existingInherits = [...(_j = existing.inherits) != null ? _j : []].sort().join(",");
|
|
427
|
+
const configInherits = [...(_k = role.inherits) != null ? _k : []].sort().join(",");
|
|
362
428
|
if (existingInherits !== configInherits) {
|
|
363
429
|
changes.push("inherits changed");
|
|
364
430
|
}
|
|
431
|
+
const existingDefault = (_l = existing.isDefault) != null ? _l : false;
|
|
432
|
+
const configDefault = (_m = role.isDefault) != null ? _m : false;
|
|
433
|
+
if (existingDefault !== configDefault) {
|
|
434
|
+
changes.push(`isDefault: ${existingDefault} -> ${configDefault}`);
|
|
435
|
+
}
|
|
365
436
|
if (changes.length > 0) {
|
|
366
437
|
roleOps.push({ type: "update", key, role, existing, changes });
|
|
367
438
|
}
|
|
@@ -371,7 +442,7 @@ function computeRbacDiff(config, server, memberCounts) {
|
|
|
371
442
|
for (const [key, role] of serverRoleByKey) {
|
|
372
443
|
if (renameMap.has(key)) continue;
|
|
373
444
|
if (role.isActive) {
|
|
374
|
-
const members = (
|
|
445
|
+
const members = (_n = memberCounts[role.configKey]) != null ? _n : 0;
|
|
375
446
|
if (members > 0) hasDestructive = true;
|
|
376
447
|
roleOps.push({ type: "archive", key, existing: role, assignedMembers: members });
|
|
377
448
|
}
|
|
@@ -407,6 +478,11 @@ var RbacSyncClient = class {
|
|
|
407
478
|
constructor(config) {
|
|
408
479
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
409
480
|
this.apiKey = config.apiKey;
|
|
481
|
+
if (!this.baseUrl.startsWith("https://") && !this.baseUrl.startsWith("http://localhost") && !this.baseUrl.startsWith("http://127.0.0.1")) {
|
|
482
|
+
console.warn(
|
|
483
|
+
"WARNING: AUTHGATE_BASE_URL does not use HTTPS. API key may be transmitted insecurely."
|
|
484
|
+
);
|
|
485
|
+
}
|
|
410
486
|
}
|
|
411
487
|
async request(method, path, body) {
|
|
412
488
|
var _a, _b;
|
|
@@ -425,9 +501,12 @@ var RbacSyncClient = class {
|
|
|
425
501
|
let message;
|
|
426
502
|
try {
|
|
427
503
|
const json = JSON.parse(text);
|
|
428
|
-
message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b :
|
|
504
|
+
message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : "Unknown error";
|
|
429
505
|
} catch (e) {
|
|
430
|
-
message = text;
|
|
506
|
+
message = text.length > 500 ? text.slice(0, 500) + "..." : text;
|
|
507
|
+
}
|
|
508
|
+
if (this.apiKey) {
|
|
509
|
+
message = message.replaceAll(this.apiKey, "[REDACTED]");
|
|
431
510
|
}
|
|
432
511
|
throw new Error(`API error (${res.status}): ${message}`);
|
|
433
512
|
}
|
|
@@ -447,7 +526,10 @@ var RbacSyncClient = class {
|
|
|
447
526
|
resources: config.resources,
|
|
448
527
|
roles: config.roles,
|
|
449
528
|
conditions: config.conditions ? Object.fromEntries(
|
|
450
|
-
Object.entries(config.conditions).map(([k]) => [
|
|
529
|
+
Object.entries(config.conditions).map(([k, fn]) => [
|
|
530
|
+
k,
|
|
531
|
+
{ key: k, sourceHash: hashConditionSource(fn) }
|
|
532
|
+
])
|
|
451
533
|
) : void 0,
|
|
452
534
|
force
|
|
453
535
|
});
|
|
@@ -510,6 +592,9 @@ function formatRbacDiff(diff, dryRun) {
|
|
|
510
592
|
import_chalk.default.dim(` inherits: [${[...op.role.inherits].join(", ")}]`)
|
|
511
593
|
);
|
|
512
594
|
}
|
|
595
|
+
if (op.role.isDefault) {
|
|
596
|
+
lines.push(import_chalk.default.dim(` isDefault: true`));
|
|
597
|
+
}
|
|
513
598
|
} else if (op.type === "update") {
|
|
514
599
|
lines.push(import_chalk.default.yellow(` ~ UPDATE role "${op.key}"`));
|
|
515
600
|
for (const change of op.changes) {
|
|
@@ -580,8 +665,98 @@ function formatRbacDiff(diff, dryRun) {
|
|
|
580
665
|
return lines.join("\n");
|
|
581
666
|
}
|
|
582
667
|
|
|
668
|
+
// src/role-management.ts
|
|
669
|
+
function createRoleManagement(config) {
|
|
670
|
+
const baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
671
|
+
const apiKey = config.apiKey;
|
|
672
|
+
async function request(method, path, body) {
|
|
673
|
+
var _a, _b;
|
|
674
|
+
const url = `${baseUrl}${path}`;
|
|
675
|
+
const headers = {
|
|
676
|
+
Authorization: `Bearer ${apiKey}`,
|
|
677
|
+
"Content-Type": "application/json"
|
|
678
|
+
};
|
|
679
|
+
const res = await fetch(url, {
|
|
680
|
+
method,
|
|
681
|
+
headers,
|
|
682
|
+
body: body ? JSON.stringify(body) : void 0
|
|
683
|
+
});
|
|
684
|
+
if (!res.ok) {
|
|
685
|
+
const text = await res.text();
|
|
686
|
+
let message;
|
|
687
|
+
try {
|
|
688
|
+
const json = JSON.parse(text);
|
|
689
|
+
message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : "Unknown error";
|
|
690
|
+
} catch (e) {
|
|
691
|
+
message = text.length > 500 ? text.slice(0, 500) + "..." : text;
|
|
692
|
+
}
|
|
693
|
+
if (apiKey) {
|
|
694
|
+
message = message.replaceAll(apiKey, "[REDACTED]");
|
|
695
|
+
}
|
|
696
|
+
throw new Error(`API error (${res.status}): ${message}`);
|
|
697
|
+
}
|
|
698
|
+
return res.json();
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
async listRoles() {
|
|
702
|
+
const result = await request("GET", "/api/v1/roles");
|
|
703
|
+
return result.data.map(toRole);
|
|
704
|
+
},
|
|
705
|
+
async createRole(input) {
|
|
706
|
+
var _a, _b;
|
|
707
|
+
const result = await request("POST", "/api/v1/roles", {
|
|
708
|
+
key: input.key,
|
|
709
|
+
name: input.name,
|
|
710
|
+
description: input.description,
|
|
711
|
+
is_default: (_a = input.isDefault) != null ? _a : false,
|
|
712
|
+
permissions: (_b = input.permissions) != null ? _b : []
|
|
713
|
+
});
|
|
714
|
+
return toRole(result.role);
|
|
715
|
+
},
|
|
716
|
+
async updateRole(roleId, input) {
|
|
717
|
+
const body = {};
|
|
718
|
+
if (input.name !== void 0) body.name = input.name;
|
|
719
|
+
if (input.description !== void 0) body.description = input.description;
|
|
720
|
+
if (input.permissions !== void 0) body.permissions = input.permissions;
|
|
721
|
+
if (input.isDefault !== void 0) body.is_default = input.isDefault;
|
|
722
|
+
const result = await request(
|
|
723
|
+
"PATCH",
|
|
724
|
+
`/api/v1/roles/${encodeURIComponent(roleId)}`,
|
|
725
|
+
body
|
|
726
|
+
);
|
|
727
|
+
return toRole(result.role);
|
|
728
|
+
},
|
|
729
|
+
async deleteRole(roleId) {
|
|
730
|
+
await request(
|
|
731
|
+
"DELETE",
|
|
732
|
+
`/api/v1/roles/${encodeURIComponent(roleId)}`
|
|
733
|
+
);
|
|
734
|
+
},
|
|
735
|
+
async setDefaultRole(roleId) {
|
|
736
|
+
return this.updateRole(roleId, { isDefault: true });
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function toRole(raw) {
|
|
741
|
+
return {
|
|
742
|
+
id: raw.id,
|
|
743
|
+
projectId: raw.project_id,
|
|
744
|
+
key: raw.key,
|
|
745
|
+
name: raw.name,
|
|
746
|
+
description: raw.description,
|
|
747
|
+
isDefault: raw.is_default,
|
|
748
|
+
permissions: raw.permissions,
|
|
749
|
+
createdAt: raw.created_at,
|
|
750
|
+
updatedAt: raw.updated_at
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
583
754
|
// src/index.ts
|
|
584
|
-
function defineRbac(config) {
|
|
755
|
+
function defineRbac(config, opts) {
|
|
756
|
+
var _a;
|
|
757
|
+
if ((opts == null ? void 0 : opts.validate) !== false) {
|
|
758
|
+
validateRbacConfig(config);
|
|
759
|
+
}
|
|
585
760
|
const resources = {};
|
|
586
761
|
for (const [key, resource] of Object.entries(config.resources)) {
|
|
587
762
|
const actions = {};
|
|
@@ -592,7 +767,7 @@ function defineRbac(config) {
|
|
|
592
767
|
}
|
|
593
768
|
const roles = {};
|
|
594
769
|
for (const [key, role] of Object.entries(config.roles)) {
|
|
595
|
-
roles[key] = { key, name: role.name, grants: role.grants };
|
|
770
|
+
roles[key] = { key, name: role.name, grants: role.grants, isDefault: (_a = role.isDefault) != null ? _a : false };
|
|
596
771
|
}
|
|
597
772
|
const permissions = {};
|
|
598
773
|
for (const [key, resource] of Object.entries(config.resources)) {
|
|
@@ -615,6 +790,7 @@ function defineRbac(config) {
|
|
|
615
790
|
0 && (module.exports = {
|
|
616
791
|
RbacSyncClient,
|
|
617
792
|
computeRbacDiff,
|
|
793
|
+
createRoleManagement,
|
|
618
794
|
defineRbac,
|
|
619
795
|
formatRbacDiff,
|
|
620
796
|
hashConditionSource,
|
package/dist/index.d.cts
CHANGED
|
@@ -39,6 +39,8 @@ interface RoleConfig {
|
|
|
39
39
|
description?: string;
|
|
40
40
|
inherits?: readonly string[];
|
|
41
41
|
grants: Record<string, Record<string, GrantValue>>;
|
|
42
|
+
/** Mark this role as the default for new org members. Only one role may be default. */
|
|
43
|
+
isDefault?: boolean;
|
|
42
44
|
/** Previous config key if this role was renamed. */
|
|
43
45
|
renamedFrom?: string;
|
|
44
46
|
}
|
|
@@ -80,6 +82,63 @@ type InferScopes<T, Resource extends string> = T extends {
|
|
|
80
82
|
type InferConditionKeys<T> = T extends {
|
|
81
83
|
conditions: infer C;
|
|
82
84
|
} ? keyof C & string : never;
|
|
85
|
+
/** Extract role keys that have `isDefault: true`. */
|
|
86
|
+
type DefaultRoleKeys<Roles> = {
|
|
87
|
+
[K in keyof Roles]: Roles[K] extends {
|
|
88
|
+
isDefault: true;
|
|
89
|
+
} ? K : never;
|
|
90
|
+
}[keyof Roles];
|
|
91
|
+
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
92
|
+
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
|
|
93
|
+
/**
|
|
94
|
+
* Compile-time constraint: if more than one role has `isDefault: true`,
|
|
95
|
+
* intersect those roles' `isDefault` with an error string so TypeScript
|
|
96
|
+
* produces a descriptive message.
|
|
97
|
+
*
|
|
98
|
+
* Error reads: Type 'true' is not assignable to type '"ERROR: ..."'.
|
|
99
|
+
*/
|
|
100
|
+
type ValidateSingleDefault<T> = T extends {
|
|
101
|
+
roles: infer Roles;
|
|
102
|
+
} ? IsUnion<DefaultRoleKeys<Roles>> extends true ? {
|
|
103
|
+
roles: {
|
|
104
|
+
[K in DefaultRoleKeys<Roles> & keyof Roles]: {
|
|
105
|
+
isDefault: "ERROR: Only one role may have isDefault: true";
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
} : {} : {};
|
|
109
|
+
/** Extract valid action string literals from a resource config. */
|
|
110
|
+
type ValidActions<R> = R extends {
|
|
111
|
+
actions: readonly (infer U)[];
|
|
112
|
+
} ? U & string : never;
|
|
113
|
+
/**
|
|
114
|
+
* Compile-time constraint: validates grant resource/action keys against
|
|
115
|
+
* declared resources at the type level.
|
|
116
|
+
*
|
|
117
|
+
* - **Invalid resource key** → descriptive error naming the resource.
|
|
118
|
+
* - **Invalid action key** → descriptive error listing valid actions.
|
|
119
|
+
* - **Valid grants** → passes through original types transparently.
|
|
120
|
+
*
|
|
121
|
+
* Used as `T & ValidateConfig<T>` in the `defineRbac()` parameter type.
|
|
122
|
+
*
|
|
123
|
+
* @note Extra properties on role objects (e.g. `isFakeProp`) are caught
|
|
124
|
+
* by the runtime `validateRbacConfig()` which runs by default.
|
|
125
|
+
*/
|
|
126
|
+
type ValidateConfig<T> = T extends {
|
|
127
|
+
resources: infer R extends Record<string, ResourceConfig>;
|
|
128
|
+
roles: infer Roles;
|
|
129
|
+
} ? {
|
|
130
|
+
roles: {
|
|
131
|
+
[RK in keyof Roles]: Roles[RK] extends {
|
|
132
|
+
grants: infer G;
|
|
133
|
+
} ? {
|
|
134
|
+
grants: {
|
|
135
|
+
[ResK in keyof G]: ResK extends keyof R ? keyof G[ResK] extends ValidActions<R[ResK & keyof R]> ? {
|
|
136
|
+
[ActK in keyof G[ResK]]: G[ResK][ActK];
|
|
137
|
+
} : `ERROR: invalid action(s) in "${ResK & string}" grants. Valid actions: ${ValidActions<R[ResK & keyof R]>}` : `ERROR: resource "${ResK & string}" is not declared in resources`;
|
|
138
|
+
};
|
|
139
|
+
} : Roles[RK];
|
|
140
|
+
};
|
|
141
|
+
} : {};
|
|
83
142
|
/** A structured resource with a `key` constant and an `actions` map of `"resource:action"` strings. */
|
|
84
143
|
type StructuredResource<T, K extends string> = {
|
|
85
144
|
readonly key: K;
|
|
@@ -91,7 +150,7 @@ type StructuredResource<T, K extends string> = {
|
|
|
91
150
|
type StructuredResources<T> = {
|
|
92
151
|
readonly [K in InferResourceKeys<T>]: StructuredResource<T, K>;
|
|
93
152
|
};
|
|
94
|
-
/** A structured role with `key`, `name`, and `
|
|
153
|
+
/** A structured role with `key`, `name`, `grants`, and optional `isDefault` from the config. */
|
|
95
154
|
type StructuredRole<T, K extends string> = {
|
|
96
155
|
readonly key: K;
|
|
97
156
|
readonly name: T extends {
|
|
@@ -104,6 +163,11 @@ type StructuredRole<T, K extends string> = {
|
|
|
104
163
|
} ? K extends keyof R ? R[K] extends {
|
|
105
164
|
grants: infer G;
|
|
106
165
|
} ? Readonly<G> : {} : {} : {};
|
|
166
|
+
readonly isDefault: T extends {
|
|
167
|
+
roles: infer R;
|
|
168
|
+
} ? K extends keyof R ? R[K] extends {
|
|
169
|
+
isDefault: infer D;
|
|
170
|
+
} ? D : false : false : false;
|
|
107
171
|
};
|
|
108
172
|
/** Map of all roles as structured objects: `roles.admin.key` -> `"admin"`. */
|
|
109
173
|
type StructuredRoles<T> = {
|
|
@@ -121,7 +185,7 @@ type StructuredPermissions<T> = {
|
|
|
121
185
|
* Carries phantom types that downstream factories use to constrain
|
|
122
186
|
* permission/role string parameters -- enabling full IDE autocomplete.
|
|
123
187
|
*/
|
|
124
|
-
interface TypedRbac<T
|
|
188
|
+
interface TypedRbac<T> {
|
|
125
189
|
/** The raw RBAC config. Access resources, roles, etc. via `rbac._config`. */
|
|
126
190
|
readonly _config: T;
|
|
127
191
|
/**
|
|
@@ -172,6 +236,7 @@ interface ServerRole {
|
|
|
172
236
|
grants: Record<string, Record<string, GrantValue>> | null;
|
|
173
237
|
inherits: string[];
|
|
174
238
|
resolvedPermissions: string[];
|
|
239
|
+
isDefault: boolean;
|
|
175
240
|
isActive: boolean;
|
|
176
241
|
managedBy: string;
|
|
177
242
|
version: number;
|
|
@@ -245,7 +310,7 @@ interface DiffResult {
|
|
|
245
310
|
conditionOps: ConditionOp[];
|
|
246
311
|
hasDestructive: boolean;
|
|
247
312
|
}
|
|
248
|
-
/**
|
|
313
|
+
/** Hash of a condition function's source code for change detection. */
|
|
249
314
|
declare function hashConditionSource(fn: Function): string;
|
|
250
315
|
declare function computeRbacDiff(config: RbacConfig, server: ServerState, memberCounts: Record<string, number>): DiffResult;
|
|
251
316
|
|
|
@@ -272,12 +337,61 @@ declare class RbacSyncClient {
|
|
|
272
337
|
|
|
273
338
|
declare function formatRbacDiff(diff: DiffResult, dryRun: boolean): string;
|
|
274
339
|
|
|
340
|
+
interface RoleManagementConfig {
|
|
341
|
+
baseUrl: string;
|
|
342
|
+
apiKey: string;
|
|
343
|
+
}
|
|
344
|
+
interface Role {
|
|
345
|
+
id: string;
|
|
346
|
+
projectId: string;
|
|
347
|
+
key: string;
|
|
348
|
+
name: string;
|
|
349
|
+
description: string | null;
|
|
350
|
+
isDefault: boolean;
|
|
351
|
+
permissions: string[];
|
|
352
|
+
createdAt: string;
|
|
353
|
+
updatedAt: string;
|
|
354
|
+
}
|
|
355
|
+
interface CreateRoleInput {
|
|
356
|
+
key: string;
|
|
357
|
+
name: string;
|
|
358
|
+
description?: string;
|
|
359
|
+
isDefault?: boolean;
|
|
360
|
+
permissions?: string[];
|
|
361
|
+
}
|
|
362
|
+
interface UpdateRoleInput {
|
|
363
|
+
name?: string;
|
|
364
|
+
description?: string | null;
|
|
365
|
+
permissions?: string[];
|
|
366
|
+
isDefault?: boolean;
|
|
367
|
+
}
|
|
368
|
+
interface RoleManagementClient {
|
|
369
|
+
listRoles(): Promise<Role[]>;
|
|
370
|
+
createRole(input: CreateRoleInput): Promise<Role>;
|
|
371
|
+
updateRole(roleId: string, input: UpdateRoleInput): Promise<Role>;
|
|
372
|
+
deleteRole(roleId: string): Promise<void>;
|
|
373
|
+
setDefaultRole(roleId: string): Promise<Role>;
|
|
374
|
+
}
|
|
375
|
+
declare function createRoleManagement(config: RoleManagementConfig): RoleManagementClient;
|
|
376
|
+
|
|
377
|
+
/** Base config shape used as the generic constraint for `defineRbac()`. */
|
|
378
|
+
type DefineRbacConfig = {
|
|
379
|
+
resources: Record<string, ResourceConfig>;
|
|
380
|
+
conditions?: Record<string, ConditionFn>;
|
|
381
|
+
roles: Record<string, {
|
|
382
|
+
name: string;
|
|
383
|
+
description?: string;
|
|
384
|
+
inherits?: readonly string[];
|
|
385
|
+
isDefault?: boolean;
|
|
386
|
+
renamedFrom?: string;
|
|
387
|
+
grants: Record<string, Record<string, GrantValue>>;
|
|
388
|
+
}>;
|
|
389
|
+
};
|
|
275
390
|
/**
|
|
276
391
|
* Define your RBAC config. Use this in your `authgate.rbac.ts` config file.
|
|
277
392
|
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* and server-side checks.
|
|
393
|
+
* Grants are validated at compile-time: if a role references an undeclared
|
|
394
|
+
* resource or action, TypeScript flags it as a type error.
|
|
281
395
|
*
|
|
282
396
|
* @example
|
|
283
397
|
* ```ts
|
|
@@ -310,6 +424,8 @@ declare function formatRbacDiff(diff: DiffResult, dryRun: boolean): string;
|
|
|
310
424
|
* rbac.permissions.documents.write // "documents:write"
|
|
311
425
|
* ```
|
|
312
426
|
*/
|
|
313
|
-
declare function defineRbac<const T extends
|
|
427
|
+
declare function defineRbac<const T extends DefineRbacConfig>(config: T & ValidateConfig<T> & ValidateSingleDefault<T>, opts?: {
|
|
428
|
+
validate?: boolean;
|
|
429
|
+
}): TypedRbac<T>;
|
|
314
430
|
|
|
315
|
-
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 };
|
|
431
|
+
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, validateRbacConfig };
|