@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/index.d.ts 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 `grants` from the config. */
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 extends RbacConfig> {
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
- /** Simple hash of a condition function's source code for change detection. */
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
- * 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.
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 RbacConfig>(config: T): TypedRbac<T>;
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 };
package/dist/index.mjs CHANGED
@@ -5,10 +5,100 @@ import {
5
5
  hashConditionSource,
6
6
  loadRbacConfig,
7
7
  validateRbacConfig
8
- } from "./chunk-HE57TIQI.mjs";
8
+ } from "./chunk-ZFKXT2MP.mjs";
9
+
10
+ // src/role-management.ts
11
+ function createRoleManagement(config) {
12
+ const baseUrl = config.baseUrl.replace(/\/$/, "");
13
+ const apiKey = config.apiKey;
14
+ async function request(method, path, body) {
15
+ var _a, _b;
16
+ const url = `${baseUrl}${path}`;
17
+ const headers = {
18
+ Authorization: `Bearer ${apiKey}`,
19
+ "Content-Type": "application/json"
20
+ };
21
+ const res = await fetch(url, {
22
+ method,
23
+ headers,
24
+ body: body ? JSON.stringify(body) : void 0
25
+ });
26
+ if (!res.ok) {
27
+ const text = await res.text();
28
+ let message;
29
+ try {
30
+ const json = JSON.parse(text);
31
+ message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : "Unknown error";
32
+ } catch (e) {
33
+ message = text.length > 500 ? text.slice(0, 500) + "..." : text;
34
+ }
35
+ if (apiKey) {
36
+ message = message.replaceAll(apiKey, "[REDACTED]");
37
+ }
38
+ throw new Error(`API error (${res.status}): ${message}`);
39
+ }
40
+ return res.json();
41
+ }
42
+ return {
43
+ async listRoles() {
44
+ const result = await request("GET", "/api/v1/roles");
45
+ return result.data.map(toRole);
46
+ },
47
+ async createRole(input) {
48
+ var _a, _b;
49
+ const result = await request("POST", "/api/v1/roles", {
50
+ key: input.key,
51
+ name: input.name,
52
+ description: input.description,
53
+ is_default: (_a = input.isDefault) != null ? _a : false,
54
+ permissions: (_b = input.permissions) != null ? _b : []
55
+ });
56
+ return toRole(result.role);
57
+ },
58
+ async updateRole(roleId, input) {
59
+ const body = {};
60
+ if (input.name !== void 0) body.name = input.name;
61
+ if (input.description !== void 0) body.description = input.description;
62
+ if (input.permissions !== void 0) body.permissions = input.permissions;
63
+ if (input.isDefault !== void 0) body.is_default = input.isDefault;
64
+ const result = await request(
65
+ "PATCH",
66
+ `/api/v1/roles/${encodeURIComponent(roleId)}`,
67
+ body
68
+ );
69
+ return toRole(result.role);
70
+ },
71
+ async deleteRole(roleId) {
72
+ await request(
73
+ "DELETE",
74
+ `/api/v1/roles/${encodeURIComponent(roleId)}`
75
+ );
76
+ },
77
+ async setDefaultRole(roleId) {
78
+ return this.updateRole(roleId, { isDefault: true });
79
+ }
80
+ };
81
+ }
82
+ function toRole(raw) {
83
+ return {
84
+ id: raw.id,
85
+ projectId: raw.project_id,
86
+ key: raw.key,
87
+ name: raw.name,
88
+ description: raw.description,
89
+ isDefault: raw.is_default,
90
+ permissions: raw.permissions,
91
+ createdAt: raw.created_at,
92
+ updatedAt: raw.updated_at
93
+ };
94
+ }
9
95
 
10
96
  // src/index.ts
11
- function defineRbac(config) {
97
+ function defineRbac(config, opts) {
98
+ var _a;
99
+ if ((opts == null ? void 0 : opts.validate) !== false) {
100
+ validateRbacConfig(config);
101
+ }
12
102
  const resources = {};
13
103
  for (const [key, resource] of Object.entries(config.resources)) {
14
104
  const actions = {};
@@ -19,7 +109,7 @@ function defineRbac(config) {
19
109
  }
20
110
  const roles = {};
21
111
  for (const [key, role] of Object.entries(config.roles)) {
22
- roles[key] = { key, name: role.name, grants: role.grants };
112
+ roles[key] = { key, name: role.name, grants: role.grants, isDefault: (_a = role.isDefault) != null ? _a : false };
23
113
  }
24
114
  const permissions = {};
25
115
  for (const [key, resource] of Object.entries(config.resources)) {
@@ -41,6 +131,7 @@ function defineRbac(config) {
41
131
  export {
42
132
  RbacSyncClient,
43
133
  computeRbacDiff,
134
+ createRoleManagement,
44
135
  defineRbac,
45
136
  formatRbacDiff,
46
137
  hashConditionSource,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auth-gate/rbac",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {