@nebutra/permissions 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- > **Status: Foundation** — CASL in-process evaluation supports deterministic role inheritance, ABAC conditions, and field-level rules. OpenFGA remains a stub, so this package is not yet production-ready for Zanzibar-style relationship authorization.
1
+ > **Status: Foundation** — CASL in-process evaluation supports deterministic role inheritance, ABAC conditions, and field-level rules. OpenFGA support now uses the store-scoped REST API, but model and tuple lifecycle management remain external to this package.
2
2
 
3
3
  # @nebutra/permissions
4
4
 
@@ -7,7 +7,7 @@ RBAC (Role-Based Access Control) and ABAC (Attribute-Based Access Control) permi
7
7
  ## Features
8
8
 
9
9
  - **CASL-based in-process evaluation** — Fast, no network calls, great for UI + API middleware
10
- - **OpenFGA integration** — Placeholder for managed/self-hosted Zanzibar relationship graphs
10
+ - **OpenFGA integration** — Store-scoped REST checks, writes, deletes, and list-objects calls for managed/self-hosted Zanzibar relationship graphs
11
11
  - **Role hierarchy** — Roles inherit permissions from parent roles; child rules override inherited grants deterministically
12
12
  - **ABAC conditions** — Dynamic field resolution at evaluation time
13
13
  - **Field-level permissions** — Restrict access to specific fields through CASL-backed checks
@@ -186,13 +186,17 @@ const provider = createCASLProvider();
186
186
 
187
187
  ### OpenFGA (Relationship-Based)
188
188
 
189
- OpenFGA is still a foundation stub and should not be used for production relationship authorization yet.
189
+ OpenFGA uses the store-scoped REST API and fails closed when configuration or network calls are invalid. This package does not manage OpenFGA authorization models or tuple migrations.
190
190
 
191
191
  ```typescript
192
192
  import { createOpenFGAProvider } from "@nebutra/permissions/openfga";
193
193
 
194
194
  const provider = createOpenFGAProvider(
195
- "http://openfga.internal:8080",
195
+ {
196
+ apiUrl: "http://openfga.internal:8080",
197
+ storeId: "store_abc123",
198
+ authToken: process.env.OPENFGA_AUTH_TOKEN,
199
+ },
196
200
  roles
197
201
  );
198
202
 
@@ -220,7 +224,7 @@ PERMISSIONS_PROVIDER=casl # "casl" | "openfga"
220
224
 
221
225
  # OpenFGA configuration
222
226
  OPENFGA_API_URL=http://openfga:8080 # Triggers OpenFGA provider
223
- OPENFGA_STORE_ID=abc123 # Optional
227
+ OPENFGA_STORE_ID=abc123 # Required for OpenFGA REST calls
224
228
  OPENFGA_AUTH_TOKEN=secret # Optional for managed OpenFGA
225
229
  ```
226
230
 
@@ -232,9 +236,11 @@ Initialize the global permissions manager (singleton).
232
236
 
233
237
  ```typescript
234
238
  const permissions = createPermissions({
235
- provider: "casl",
239
+ provider: "openfga",
236
240
  roles: customRoles,
237
241
  openFgaApiUrl: "http://openfga:8080",
242
+ openFgaStoreId: "abc123",
243
+ openFgaAuthToken: process.env.OPENFGA_AUTH_TOKEN,
238
244
  });
239
245
  ```
240
246
 
@@ -0,0 +1,18 @@
1
+ import type { Action, PermissionConfig, PermissionContext, PermissionProvider, PermissionProviderType, PermissionRule, Resource, RoleDefinition } from "./types";
2
+ export declare class PermissionsManager {
3
+ private provider;
4
+ private providerType;
5
+ constructor(config?: PermissionConfig);
6
+ private detectProvider;
7
+ getProviderType(): PermissionProviderType;
8
+ can(context: PermissionContext, action: Action, resource: Resource, subject?: unknown, field?: string): boolean;
9
+ cannot(context: PermissionContext, action: Action, resource: Resource, subject?: unknown, field?: string): boolean;
10
+ getRulesForRole(role: string): PermissionRule[];
11
+ defineRole(definition: RoleDefinition): void;
12
+ buildAbilityFor(context: PermissionContext): unknown;
13
+ getProvider(): PermissionProvider;
14
+ }
15
+ export declare function createPermissions(config?: PermissionConfig): PermissionsManager;
16
+ export declare function getPermissions(): PermissionsManager;
17
+ export declare function resetPermissions(): void;
18
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,MAAM,EACN,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,cAAc,EACd,QAAQ,EACR,cAAc,EACf,MAAM,SAAS,CAAC;AAIjB,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,YAAY,CAAyB;gBAEjC,MAAM,CAAC,EAAE,gBAAgB;IAyBrC,OAAO,CAAC,cAAc;IAmBtB,eAAe,IAAI,sBAAsB;IAIzC,GAAG,CACD,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO;IAIV,MAAM,CACJ,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO;IAIV,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE;IAI/C,UAAU,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI;IAI5C,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO;IAIpD,WAAW,IAAI,kBAAkB;CAGlC;AAED,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,kBAAkB,CAG/E;AAED,wBAAgB,cAAc,IAAI,kBAAkB,CAKnD;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
@@ -0,0 +1,80 @@
1
+ import { logger } from "@nebutra/logger";
2
+ import { CASLProvider } from "./providers/casl";
3
+ import { OpenFGAProvider } from "./providers/openfga";
4
+ import { getDefaultRoles } from "./roles";
5
+ let permissionsInstance = null;
6
+ export class PermissionsManager {
7
+ provider;
8
+ providerType;
9
+ constructor(config) {
10
+ const detectedType = this.detectProvider(config?.provider);
11
+ this.providerType = detectedType;
12
+ const roles = config?.roles || getDefaultRoles();
13
+ if (detectedType === "openfga") {
14
+ // exactOptionalPropertyTypes: only set keys that have defined values so
15
+ // the optional `?:` modifier on OpenFGAProviderConfig stays satisfied.
16
+ const openFgaConfig = {};
17
+ if (config?.openFgaApiUrl !== undefined)
18
+ openFgaConfig.apiUrl = config.openFgaApiUrl;
19
+ if (config?.openFgaAuthToken !== undefined)
20
+ openFgaConfig.authToken = config.openFgaAuthToken;
21
+ if (config?.openFgaStoreId !== undefined)
22
+ openFgaConfig.storeId = config.openFgaStoreId;
23
+ this.provider = new OpenFGAProvider(openFgaConfig, roles);
24
+ logger.info("Initialized OpenFGA permissions provider");
25
+ }
26
+ else {
27
+ this.provider = new CASLProvider(roles);
28
+ logger.info("Initialized CASL permissions provider");
29
+ }
30
+ }
31
+ detectProvider(explicit) {
32
+ if (explicit) {
33
+ return explicit;
34
+ }
35
+ if (process.env.PERMISSIONS_PROVIDER) {
36
+ const provider = process.env.PERMISSIONS_PROVIDER;
37
+ if (["casl", "openfga"].includes(provider)) {
38
+ return provider;
39
+ }
40
+ }
41
+ if (process.env.OPENFGA_API_URL) {
42
+ return "openfga";
43
+ }
44
+ return "casl";
45
+ }
46
+ getProviderType() {
47
+ return this.providerType;
48
+ }
49
+ can(context, action, resource, subject, field) {
50
+ return this.provider.can(context, action, resource, subject, field);
51
+ }
52
+ cannot(context, action, resource, subject, field) {
53
+ return this.provider.cannot(context, action, resource, subject, field);
54
+ }
55
+ getRulesForRole(role) {
56
+ return this.provider.getRulesForRole(role);
57
+ }
58
+ defineRole(definition) {
59
+ this.provider.defineRole(definition);
60
+ }
61
+ buildAbilityFor(context) {
62
+ return this.provider.buildAbilityFor(context);
63
+ }
64
+ getProvider() {
65
+ return this.provider;
66
+ }
67
+ }
68
+ export function createPermissions(config) {
69
+ permissionsInstance = new PermissionsManager(config);
70
+ return permissionsInstance;
71
+ }
72
+ export function getPermissions() {
73
+ if (!permissionsInstance) {
74
+ permissionsInstance = new PermissionsManager();
75
+ }
76
+ return permissionsInstance;
77
+ }
78
+ export function resetPermissions() {
79
+ permissionsInstance = null;
80
+ }
@@ -0,0 +1,8 @@
1
+ export { createPermissions, getPermissions, PermissionsManager, resetPermissions, } from "./factory";
2
+ export { attachPermissionContext, type RequirePermissionOptions, requirePermission, } from "./middleware";
3
+ export { buildAbilityFor, CASLProvider, createCASLProvider, createPrismaAbility, getPrismaQuery, } from "./providers/casl";
4
+ export { createOpenFGAProvider, type OpenFGACheckRequest, type OpenFGACheckResponse, type OpenFGAListObjectsRequest, OpenFGAProvider, type OpenFGAProviderConfig, type OpenFGATuple, type OpenFGAWriteRequest, } from "./providers/openfga";
5
+ export { Can, Cannot, type CannotProps, type CanProps, PermissionProvider as PermissionProviderComponent, type PermissionProviderProps, type UsePermissionOptions, usePermission, usePermissionContext, } from "./react";
6
+ export { DEFAULT_ROLES, getDefaultRoles, getRoleDefinition, getRoleHierarchy, } from "./roles";
7
+ export { type AbilityOptions, type Action, ForbiddenError, type PermissionConfig, type PermissionContext, type PermissionProvider, type PermissionProviderType, type PermissionRule, type Resource, type Role, type RoleDefinition, } from "./types";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,uBAAuB,EACvB,KAAK,wBAAwB,EAC7B,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,KAAK,mBAAmB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,GAAG,EACH,MAAM,EACN,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,kBAAkB,IAAI,2BAA2B,EACjD,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,aAAa,EACb,oBAAoB,GACrB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,MAAM,EACX,cAAc,EACd,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,IAAI,EACT,KAAK,cAAc,GACpB,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { createPermissions, getPermissions, PermissionsManager, resetPermissions, } from "./factory";
2
+ export { attachPermissionContext, requirePermission, } from "./middleware";
3
+ export { buildAbilityFor, CASLProvider, createCASLProvider, createPrismaAbility, getPrismaQuery, } from "./providers/casl";
4
+ export { createOpenFGAProvider, OpenFGAProvider, } from "./providers/openfga";
5
+ export { Can, Cannot, PermissionProvider as PermissionProviderComponent, usePermission, usePermissionContext, } from "./react";
6
+ export { DEFAULT_ROLES, getDefaultRoles, getRoleDefinition, getRoleHierarchy, } from "./roles";
7
+ export { ForbiddenError, } from "./types";
@@ -0,0 +1,12 @@
1
+ import type { Context, Next } from "hono";
2
+ import type { Action, PermissionContext, Resource } from "./types";
3
+ import { ForbiddenError } from "./types";
4
+ export interface RequirePermissionOptions {
5
+ onDenied?: (c: Context, error: ForbiddenError) => Response | Promise<Response>;
6
+ extractSubject?: (c: Context) => unknown;
7
+ }
8
+ export declare function requirePermission(action: Action, resource: Resource, options?: RequirePermissionOptions): (c: Context, next: Next) => Promise<void | Response>;
9
+ export declare function attachPermissionContext(extractUser: (c: Context) => PermissionContext | Promise<PermissionContext>): (c: Context, next: Next) => Promise<void | (Response & import("hono").TypedResponse<{
10
+ error: string;
11
+ }, 500, "json">)>;
12
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,cAAc,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/E,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC;CAC1C;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,wBAAwB,IAEpB,GAAG,OAAO,EAAE,MAAM,IAAI,8BAoCrC;AAED,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,IAE7D,GAAG,OAAO,EAAE,MAAM,IAAI;;kBAWrC"}
@@ -0,0 +1,45 @@
1
+ import { logger } from "@nebutra/logger";
2
+ import { getPermissions } from "./factory";
3
+ import { ForbiddenError } from "./types";
4
+ export function requirePermission(action, resource, options) {
5
+ return async (c, next) => {
6
+ const permissionsManager = getPermissions();
7
+ const user = c.get("user");
8
+ if (!user) {
9
+ logger.warn("requirePermission: no user context found");
10
+ return c.json({ error: "Unauthorized" }, 401);
11
+ }
12
+ const subject = options?.extractSubject?.(c);
13
+ const canAccess = permissionsManager.can(user, action, resource, subject);
14
+ if (!canAccess) {
15
+ const error = new ForbiddenError(action, resource, user);
16
+ logger.info("Permission denied", {
17
+ userId: user.userId,
18
+ action,
19
+ resource,
20
+ subject,
21
+ });
22
+ if (options?.onDenied) {
23
+ return options.onDenied(c, error);
24
+ }
25
+ return c.json({
26
+ error: "Forbidden",
27
+ message: error.message,
28
+ }, 403);
29
+ }
30
+ return next();
31
+ };
32
+ }
33
+ export function attachPermissionContext(extractUser) {
34
+ return async (c, next) => {
35
+ try {
36
+ const user = await extractUser(c);
37
+ c.set("user", user);
38
+ }
39
+ catch (error) {
40
+ logger.error("Failed to extract user context", { error });
41
+ return c.json({ error: "Internal Server Error" }, 500);
42
+ }
43
+ return next();
44
+ };
45
+ }
@@ -0,0 +1,28 @@
1
+ import { type Ability } from "@casl/ability";
2
+ import { type PrismaQuery } from "@casl/prisma";
3
+ import type { Action, PermissionContext, PermissionProvider, PermissionRule, Resource, RoleDefinition } from "../types";
4
+ type AppAbility = Ability<[Action, Resource]>;
5
+ export declare class CASLProvider implements PermissionProvider {
6
+ private abilities;
7
+ private roleDefinitions;
8
+ constructor(roles?: RoleDefinition[]);
9
+ defineRole(definition: RoleDefinition): void;
10
+ getRulesForRole(role: string): PermissionRule[];
11
+ buildAbilityFor(context: PermissionContext): AppAbility;
12
+ can(context: PermissionContext, action: Action, resource: Resource, subject?: unknown, field?: string): boolean;
13
+ cannot(context: PermissionContext, action: Action, resource: Resource, subject?: unknown, field?: string): boolean;
14
+ private collectRulesForContext;
15
+ private applyRule;
16
+ private getRoleHierarchy;
17
+ private resolveConditions;
18
+ private resolvePath;
19
+ private resolveSubject;
20
+ private getCacheKey;
21
+ clearCache(): void;
22
+ }
23
+ export declare function createCASLProvider(roles?: RoleDefinition[]): CASLProvider;
24
+ export declare function buildAbilityFor(context: PermissionContext, roles?: RoleDefinition[]): AppAbility;
25
+ export declare function createPrismaAbility(context: PermissionContext, roles?: RoleDefinition[]): AppAbility;
26
+ export declare function getPrismaQuery(ability: AppAbility, action: Action, resource: Resource): PrismaQuery;
27
+ export {};
28
+ //# sourceMappingURL=casl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"casl.d.ts","sourceRoot":"","sources":["../../src/providers/casl.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAIb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAE9D,OAAO,KAAK,EACV,MAAM,EACN,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,EACd,QAAQ,EACR,cAAc,EACf,MAAM,UAAU,CAAC;AAElB,KAAK,UAAU,GAAG,OAAO,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;AAE9C,qBAAa,YAAa,YAAW,kBAAkB;IACrD,OAAO,CAAC,SAAS,CAAsC;IACvD,OAAO,CAAC,eAAe,CAA0C;gBAErD,KAAK,CAAC,EAAE,cAAc,EAAE;IASpC,UAAU,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI;IAK5C,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE;IAc/C,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU;IAmBvD,GAAG,CACD,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO;IAKV,MAAM,CACJ,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO;IAKV,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,SAAS;IA8BjB,OAAO,CAAC,gBAAgB;IAuBxB,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,WAAW;IAInB,UAAU,IAAI,IAAI;CAGnB;AAED,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE,cAAc,EAAE,GAAG,YAAY,CAEzE;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,iBAAiB,EAAE,KAAK,CAAC,EAAE,cAAc,EAAE,GAAG,UAAU,CAGhG;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,iBAAiB,EAC1B,KAAK,CAAC,EAAE,cAAc,EAAE,GACvB,UAAU,CAEZ;AAED,wBAAgB,cAAc,CAC5B,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,GACjB,WAAW,CAOb"}
@@ -0,0 +1,152 @@
1
+ import { AbilityBuilder, subject as caslSubject, createMongoAbility, } from "@casl/ability";
2
+ import { accessibleBy } from "@casl/prisma";
3
+ import { getDefaultRoles } from "../roles";
4
+ export class CASLProvider {
5
+ abilities = new Map();
6
+ roleDefinitions = new Map();
7
+ constructor(roles) {
8
+ const defaultRoles = getDefaultRoles();
9
+ const allRoles = [...defaultRoles, ...(roles || [])];
10
+ for (const role of allRoles) {
11
+ this.roleDefinitions.set(role.role, role);
12
+ }
13
+ }
14
+ defineRole(definition) {
15
+ this.roleDefinitions.set(definition.role, definition);
16
+ this.clearCache();
17
+ }
18
+ getRulesForRole(role) {
19
+ const hierarchy = this.getRoleHierarchy(role);
20
+ const rules = [];
21
+ for (const roleInHierarchy of hierarchy) {
22
+ const definition = this.roleDefinitions.get(roleInHierarchy);
23
+ if (definition) {
24
+ rules.push(...definition.rules);
25
+ }
26
+ }
27
+ return rules;
28
+ }
29
+ buildAbilityFor(context) {
30
+ const cacheKey = this.getCacheKey(context);
31
+ const cached = this.abilities.get(cacheKey);
32
+ if (cached) {
33
+ return cached;
34
+ }
35
+ const builder = new AbilityBuilder(createMongoAbility);
36
+ const rules = this.collectRulesForContext(context);
37
+ for (const rule of rules) {
38
+ this.applyRule(builder, rule, context);
39
+ }
40
+ const ability = builder.build();
41
+ this.abilities.set(cacheKey, ability);
42
+ return ability;
43
+ }
44
+ can(context, action, resource, subject, field) {
45
+ const ability = this.buildAbilityFor(context);
46
+ return ability.can(action, this.resolveSubject(resource, subject), field);
47
+ }
48
+ cannot(context, action, resource, subject, field) {
49
+ const ability = this.buildAbilityFor(context);
50
+ return ability.cannot(action, this.resolveSubject(resource, subject), field);
51
+ }
52
+ collectRulesForContext(context) {
53
+ const rules = [];
54
+ for (const role of context.roles) {
55
+ const roleRules = this.getRulesForRole(role);
56
+ rules.push(...roleRules);
57
+ }
58
+ return rules;
59
+ }
60
+ applyRule(builder, rule, context) {
61
+ const { can, cannot } = builder;
62
+ const actions = Array.isArray(rule.action) ? rule.action : [rule.action];
63
+ const resources = Array.isArray(rule.resource) ? rule.resource : [rule.resource];
64
+ const conditions = rule.conditions
65
+ ? this.resolveConditions(rule.conditions, context)
66
+ : undefined;
67
+ const method = rule.inverted ? cannot : can;
68
+ for (const action of actions) {
69
+ for (const resource of resources) {
70
+ const args = [action, resource];
71
+ if (rule.fields) {
72
+ args.push(rule.fields);
73
+ }
74
+ if (conditions) {
75
+ args.push(conditions);
76
+ }
77
+ method.apply(builder, args);
78
+ }
79
+ }
80
+ }
81
+ getRoleHierarchy(role, visited = new Set()) {
82
+ if (visited.has(role)) {
83
+ return [];
84
+ }
85
+ visited.add(role);
86
+ const definition = this.roleDefinitions.get(role);
87
+ const inherits = definition?.inherits
88
+ ? Array.isArray(definition.inherits)
89
+ ? definition.inherits
90
+ : [definition.inherits]
91
+ : [];
92
+ const hierarchy = [];
93
+ for (const inherited of inherits) {
94
+ hierarchy.push(...this.getRoleHierarchy(inherited, visited));
95
+ }
96
+ hierarchy.push(role);
97
+ return [...new Set(hierarchy)];
98
+ }
99
+ resolveConditions(conditions, context) {
100
+ const resolved = {};
101
+ for (const [key, value] of Object.entries(conditions)) {
102
+ if (typeof value === "string" && value.startsWith("${") && value.endsWith("}")) {
103
+ const path = value.slice(2, -1);
104
+ resolved[key] = this.resolvePath(path, context);
105
+ }
106
+ else {
107
+ resolved[key] = value;
108
+ }
109
+ }
110
+ return resolved;
111
+ }
112
+ resolvePath(path, context) {
113
+ const parts = path.split(".");
114
+ let current = context;
115
+ for (const part of parts) {
116
+ if (typeof current === "object" && current !== null) {
117
+ current = current[part];
118
+ }
119
+ else {
120
+ return undefined;
121
+ }
122
+ }
123
+ return current;
124
+ }
125
+ resolveSubject(resource, subject) {
126
+ if (subject && typeof subject === "object") {
127
+ return caslSubject(resource, subject);
128
+ }
129
+ return resource;
130
+ }
131
+ getCacheKey(context) {
132
+ return `${context.userId}:${context.tenantId}:${[...context.roles].sort().join(",")}`;
133
+ }
134
+ clearCache() {
135
+ this.abilities.clear();
136
+ }
137
+ }
138
+ export function createCASLProvider(roles) {
139
+ return new CASLProvider(roles);
140
+ }
141
+ export function buildAbilityFor(context, roles) {
142
+ const provider = new CASLProvider(roles);
143
+ return provider.buildAbilityFor(context);
144
+ }
145
+ export function createPrismaAbility(context, roles) {
146
+ return buildAbilityFor(context, roles);
147
+ }
148
+ export function getPrismaQuery(ability, action, resource) {
149
+ const prismaAbility = ability;
150
+ const queryByResource = accessibleBy(prismaAbility, action);
151
+ return queryByResource[resource] || {};
152
+ }
@@ -0,0 +1,58 @@
1
+ import type { Action, PermissionContext, PermissionProvider, PermissionRule, Resource, RoleDefinition } from "../types";
2
+ export interface OpenFGATuple {
3
+ user: string;
4
+ relation: string;
5
+ object: string;
6
+ }
7
+ export interface OpenFGAProviderConfig {
8
+ apiUrl?: string | undefined;
9
+ storeId?: string | undefined;
10
+ authToken?: string | undefined;
11
+ }
12
+ export interface OpenFGACheckRequest {
13
+ tuple_key: {
14
+ user: string;
15
+ relation: string;
16
+ object: string;
17
+ };
18
+ }
19
+ export interface OpenFGACheckResponse {
20
+ allowed: boolean;
21
+ }
22
+ export interface OpenFGAWriteRequest {
23
+ writes?: {
24
+ tuple_keys: OpenFGATuple[];
25
+ };
26
+ deletes?: {
27
+ tuple_keys: OpenFGATuple[];
28
+ };
29
+ }
30
+ export interface OpenFGAListObjectsRequest {
31
+ user: string;
32
+ relation: string;
33
+ type: string;
34
+ }
35
+ export declare class OpenFGAProvider implements PermissionProvider {
36
+ private apiUrl;
37
+ private storeId;
38
+ private authToken;
39
+ private roleDefinitions;
40
+ constructor(config?: string | OpenFGAProviderConfig, roles?: RoleDefinition[]);
41
+ defineRole(definition: RoleDefinition): void;
42
+ getRulesForRole(role: string): PermissionRule[];
43
+ private endpoint;
44
+ private headers;
45
+ check(user: string, relation: string, object: string): Promise<boolean>;
46
+ write(tuples: OpenFGATuple[]): Promise<void>;
47
+ delete(tuples: OpenFGATuple[]): Promise<void>;
48
+ listObjects(user: string, relation: string, type: string): Promise<string[]>;
49
+ can(_context: PermissionContext, _action: Action, _resource: Resource, _subject?: unknown): boolean;
50
+ cannot(context: PermissionContext, action: Action, resource: Resource, subject?: unknown): boolean;
51
+ canAsync(context: PermissionContext, action: Action, resource: Resource, subject?: unknown): Promise<boolean>;
52
+ private formatUser;
53
+ private formatObject;
54
+ private extractSubjectId;
55
+ buildAbilityFor(_context: PermissionContext): unknown;
56
+ }
57
+ export declare function createOpenFGAProvider(config?: string | OpenFGAProviderConfig, roles?: RoleDefinition[]): OpenFGAProvider;
58
+ //# sourceMappingURL=openfga.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openfga.d.ts","sourceRoot":"","sources":["../../src/providers/openfga.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,212 @@
1
+ import { logger } from "@nebutra/logger";
2
+ export class OpenFGAProvider {
3
+ apiUrl;
4
+ storeId;
5
+ authToken;
6
+ roleDefinitions = new Map();
7
+ constructor(config, roles) {
8
+ const resolvedConfig = typeof config === "string" ? { apiUrl: config } : config;
9
+ this.apiUrl = resolvedConfig?.apiUrl || process.env.OPENFGA_API_URL || "http://localhost:8080";
10
+ const resolvedStoreId = resolvedConfig?.storeId || process.env.OPENFGA_STORE_ID;
11
+ if (resolvedStoreId !== undefined)
12
+ this.storeId = resolvedStoreId;
13
+ const resolvedAuthToken = resolvedConfig?.authToken || process.env.OPENFGA_AUTH_TOKEN;
14
+ if (resolvedAuthToken !== undefined)
15
+ this.authToken = resolvedAuthToken;
16
+ if (roles) {
17
+ for (const role of roles) {
18
+ this.roleDefinitions.set(role.role, role);
19
+ }
20
+ }
21
+ }
22
+ defineRole(definition) {
23
+ this.roleDefinitions.set(definition.role, definition);
24
+ }
25
+ getRulesForRole(role) {
26
+ const definition = this.roleDefinitions.get(role);
27
+ return definition?.rules || [];
28
+ }
29
+ endpoint(path) {
30
+ if (!this.storeId) {
31
+ logger.error("OpenFGA store id is required", { path });
32
+ return null;
33
+ }
34
+ const apiUrl = this.apiUrl.replace(/\/+$/, "");
35
+ const storeId = encodeURIComponent(this.storeId);
36
+ return `${apiUrl}/stores/${storeId}/${path}`;
37
+ }
38
+ headers() {
39
+ const headers = { "Content-Type": "application/json" };
40
+ if (this.authToken) {
41
+ headers.Authorization = `Bearer ${this.authToken}`;
42
+ }
43
+ return headers;
44
+ }
45
+ async check(user, relation, object) {
46
+ const request = {
47
+ tuple_key: {
48
+ object,
49
+ relation,
50
+ user,
51
+ },
52
+ };
53
+ const endpoint = this.endpoint("check");
54
+ if (!endpoint) {
55
+ return false;
56
+ }
57
+ try {
58
+ const response = await fetch(endpoint, {
59
+ method: "POST",
60
+ headers: this.headers(),
61
+ body: JSON.stringify(request),
62
+ });
63
+ if (!response.ok) {
64
+ logger.error("OpenFGA check failed", { status: response.status, user, relation, object });
65
+ return false;
66
+ }
67
+ const data = await response.json();
68
+ return data.allowed === true;
69
+ }
70
+ catch (error) {
71
+ logger.error("OpenFGA check error", { error, user, relation, object });
72
+ return false;
73
+ }
74
+ }
75
+ async write(tuples) {
76
+ const request = {
77
+ writes: {
78
+ tuple_keys: tuples,
79
+ },
80
+ };
81
+ const endpoint = this.endpoint("write");
82
+ if (!endpoint) {
83
+ throw new Error("OpenFGA write failed: missing store id");
84
+ }
85
+ try {
86
+ const response = await fetch(endpoint, {
87
+ method: "POST",
88
+ headers: this.headers(),
89
+ body: JSON.stringify(request),
90
+ });
91
+ if (!response.ok) {
92
+ logger.error("OpenFGA write failed", { status: response.status });
93
+ throw new Error(`OpenFGA write failed: ${response.statusText}`);
94
+ }
95
+ }
96
+ catch (error) {
97
+ logger.error("OpenFGA write error", { error });
98
+ throw error;
99
+ }
100
+ }
101
+ async delete(tuples) {
102
+ const request = {
103
+ deletes: {
104
+ tuple_keys: tuples,
105
+ },
106
+ };
107
+ const endpoint = this.endpoint("write");
108
+ if (!endpoint) {
109
+ throw new Error("OpenFGA delete failed: missing store id");
110
+ }
111
+ try {
112
+ const response = await fetch(endpoint, {
113
+ method: "POST",
114
+ headers: this.headers(),
115
+ body: JSON.stringify(request),
116
+ });
117
+ if (!response.ok) {
118
+ logger.error("OpenFGA delete failed", { status: response.status });
119
+ throw new Error(`OpenFGA delete failed: ${response.statusText}`);
120
+ }
121
+ }
122
+ catch (error) {
123
+ logger.error("OpenFGA delete error", { error });
124
+ throw error;
125
+ }
126
+ }
127
+ async listObjects(user, relation, type) {
128
+ const request = {
129
+ relation,
130
+ type,
131
+ user,
132
+ };
133
+ const endpoint = this.endpoint("list-objects");
134
+ if (!endpoint) {
135
+ return [];
136
+ }
137
+ try {
138
+ const response = await fetch(endpoint, {
139
+ method: "POST",
140
+ headers: this.headers(),
141
+ body: JSON.stringify(request),
142
+ });
143
+ if (!response.ok) {
144
+ logger.error("OpenFGA listObjects failed", { status: response.status });
145
+ return [];
146
+ }
147
+ const data = await response.json();
148
+ return data.objects || [];
149
+ }
150
+ catch (error) {
151
+ logger.error("OpenFGA listObjects error", { error });
152
+ return [];
153
+ }
154
+ }
155
+ can(_context, _action, _resource, _subject) {
156
+ logger.warn("OpenFGAProvider.can() is synchronous wrapper - use checkAsync() for actual checks");
157
+ return false;
158
+ }
159
+ cannot(context, action, resource, subject) {
160
+ return !this.can(context, action, resource, subject);
161
+ }
162
+ async canAsync(context, action, resource, subject) {
163
+ const user = this.formatUser(context.userId);
164
+ const object = this.formatObject(resource, subject);
165
+ if (!user || !object) {
166
+ return false;
167
+ }
168
+ return this.check(user, action, object);
169
+ }
170
+ formatUser(userId) {
171
+ const trimmed = userId.trim();
172
+ if (!trimmed) {
173
+ return null;
174
+ }
175
+ return trimmed.includes(":") ? trimmed : `user:${trimmed}`;
176
+ }
177
+ formatObject(resource, subject) {
178
+ const objectId = this.extractSubjectId(subject);
179
+ if (!objectId) {
180
+ return null;
181
+ }
182
+ if (objectId.includes(":")) {
183
+ return objectId;
184
+ }
185
+ const objectType = String(resource).trim();
186
+ if (!objectType) {
187
+ return null;
188
+ }
189
+ return `${objectType}:${objectId}`;
190
+ }
191
+ extractSubjectId(subject) {
192
+ if (typeof subject === "string") {
193
+ const trimmed = subject.trim();
194
+ return trimmed || null;
195
+ }
196
+ if (subject === null || typeof subject !== "object" || Array.isArray(subject)) {
197
+ return null;
198
+ }
199
+ const id = subject.id;
200
+ if (typeof id !== "string") {
201
+ return null;
202
+ }
203
+ const trimmed = id.trim();
204
+ return trimmed || null;
205
+ }
206
+ buildAbilityFor(_context) {
207
+ return this;
208
+ }
209
+ }
210
+ export function createOpenFGAProvider(config, roles) {
211
+ return new OpenFGAProvider(config, roles);
212
+ }
@@ -0,0 +1,35 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import type { Action, PermissionProvider as IPermissionProvider, PermissionContext, Resource } from "./types";
3
+ interface PermissionContextValue {
4
+ provider: IPermissionProvider;
5
+ context: PermissionContext;
6
+ }
7
+ export interface PermissionProviderProps {
8
+ children: ReactNode;
9
+ provider: IPermissionProvider;
10
+ context: PermissionContext;
11
+ }
12
+ export declare function PermissionProvider({ children, provider, context, }: PermissionProviderProps): ReactElement;
13
+ export declare function usePermissionContext(): PermissionContextValue;
14
+ export interface UsePermissionOptions {
15
+ fallback?: boolean;
16
+ }
17
+ export declare function usePermission(action: Action, resource: Resource, subject?: unknown): boolean;
18
+ export interface CanProps {
19
+ action: Action;
20
+ resource: Resource;
21
+ subject?: unknown;
22
+ children: ReactNode;
23
+ fallback?: ReactNode;
24
+ }
25
+ export declare function Can({ action, resource, subject, children, fallback, }: CanProps): ReactElement | null;
26
+ export interface CannotProps {
27
+ action: Action;
28
+ resource: Resource;
29
+ subject?: unknown;
30
+ children: ReactNode;
31
+ fallback?: ReactNode;
32
+ }
33
+ export declare function Cannot({ action, resource, subject, children, fallback, }: CannotProps): ReactElement | null;
34
+ export {};
35
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAErD,OAAO,KAAK,EACV,MAAM,EACN,kBAAkB,IAAI,mBAAmB,EACzC,iBAAiB,EACjB,QAAQ,EACT,MAAM,SAAS,CAAC;AAEjB,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAID,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAED,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,QAAQ,EACR,OAAO,GACR,EAAE,uBAAuB,GAAG,YAAY,CAMxC;AAED,wBAAgB,oBAAoB,IAAI,sBAAsB,CAM7D;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAM5F;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,GAAG,CAAC,EAClB,MAAM,EACN,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,GACT,EAAE,QAAQ,GAAG,YAAY,GAAG,IAAI,CAQhC;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,MAAM,CAAC,EACrB,MAAM,EACN,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,GACT,EAAE,WAAW,GAAG,YAAY,GAAG,IAAI,CAQnC"}
package/dist/react.js ADDED
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { createContext, useContext, useMemo } from "react";
4
+ const PermissionContextReact = createContext(null);
5
+ export function PermissionProvider({ children, provider, context, }) {
6
+ const value = useMemo(() => ({ provider, context }), [provider, context]);
7
+ return (_jsx(PermissionContextReact.Provider, { value: value, children: children }));
8
+ }
9
+ export function usePermissionContext() {
10
+ const context = useContext(PermissionContextReact);
11
+ if (!context) {
12
+ throw new Error("usePermissionContext must be used within a PermissionProvider");
13
+ }
14
+ return context;
15
+ }
16
+ export function usePermission(action, resource, subject) {
17
+ const { provider, context } = usePermissionContext();
18
+ return useMemo(() => {
19
+ return provider.can(context, action, resource, subject);
20
+ }, [provider, context, action, resource, subject]);
21
+ }
22
+ export function Can({ action, resource, subject, children, fallback, }) {
23
+ const allowed = usePermission(action, resource, subject);
24
+ if (!allowed) {
25
+ return fallback ? fallback : null;
26
+ }
27
+ return _jsx(_Fragment, { children: children });
28
+ }
29
+ export function Cannot({ action, resource, subject, children, fallback, }) {
30
+ const allowed = usePermission(action, resource, subject);
31
+ if (allowed) {
32
+ return fallback ? fallback : null;
33
+ }
34
+ return _jsx(_Fragment, { children: children });
35
+ }
@@ -0,0 +1,6 @@
1
+ import type { RoleDefinition } from "./types";
2
+ export declare const DEFAULT_ROLES: Record<string, RoleDefinition>;
3
+ export declare function getDefaultRoles(): RoleDefinition[];
4
+ export declare function getRoleDefinition(role: string): RoleDefinition | undefined;
5
+ export declare function getRoleHierarchy(role: string): string[];
6
+ //# sourceMappingURL=roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../src/roles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAiJxD,CAAC;AAEF,wBAAgB,eAAe,IAAI,cAAc,EAAE,CAElD;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAE1E;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAcvD"}
package/dist/roles.js ADDED
@@ -0,0 +1,160 @@
1
+ export const DEFAULT_ROLES = {
2
+ owner: {
3
+ role: "owner",
4
+ description: "Full control over workspace and all resources",
5
+ rules: [
6
+ {
7
+ action: [
8
+ "create",
9
+ "read",
10
+ "update",
11
+ "delete",
12
+ "manage",
13
+ "invite",
14
+ "publish",
15
+ "export",
16
+ "share",
17
+ ],
18
+ resource: [
19
+ "Document",
20
+ "Project",
21
+ "Workspace",
22
+ "User",
23
+ "Billing",
24
+ "ApiKey",
25
+ "Team",
26
+ "Invite",
27
+ "Integration",
28
+ ],
29
+ },
30
+ ],
31
+ },
32
+ admin: {
33
+ role: "admin",
34
+ inherits: "owner",
35
+ description: "Administrative access except billing transfer and workspace deletion",
36
+ rules: [
37
+ {
38
+ action: [
39
+ "create",
40
+ "read",
41
+ "update",
42
+ "delete",
43
+ "manage",
44
+ "invite",
45
+ "publish",
46
+ "export",
47
+ "share",
48
+ ],
49
+ resource: ["Document", "Project", "User", "ApiKey", "Team", "Invite", "Integration"],
50
+ },
51
+ {
52
+ action: ["read", "update"],
53
+ resource: "Billing",
54
+ conditions: { scope: "read-only" },
55
+ },
56
+ {
57
+ action: ["delete"],
58
+ resource: "Workspace",
59
+ inverted: true,
60
+ reason: "Admins cannot delete the workspace",
61
+ },
62
+ ],
63
+ },
64
+ member: {
65
+ role: "member",
66
+ inherits: "viewer",
67
+ description: "CRUD own resources, read shared resources",
68
+ rules: [
69
+ {
70
+ action: ["create", "read", "update", "delete"],
71
+ resource: ["Document", "Project"],
72
+ conditions: { createdBy: "${user.userId}" },
73
+ },
74
+ {
75
+ action: ["read", "update"],
76
+ resource: ["Document", "Project"],
77
+ conditions: { sharedWith: "${user.userId}" },
78
+ },
79
+ {
80
+ action: ["read"],
81
+ resource: ["User", "Team"],
82
+ },
83
+ {
84
+ action: ["create", "read"],
85
+ resource: "ApiKey",
86
+ conditions: { owner: "${user.userId}" },
87
+ },
88
+ {
89
+ action: ["update", "delete"],
90
+ resource: "ApiKey",
91
+ conditions: { owner: "${user.userId}" },
92
+ },
93
+ {
94
+ action: ["invite"],
95
+ resource: "User",
96
+ },
97
+ ],
98
+ },
99
+ viewer: {
100
+ role: "viewer",
101
+ description: "Read-only access to shared resources",
102
+ rules: [
103
+ {
104
+ action: ["read"],
105
+ resource: ["Document", "Project", "User", "Team"],
106
+ conditions: { visibility: "public" },
107
+ },
108
+ {
109
+ action: ["read"],
110
+ resource: ["Document", "Project"],
111
+ conditions: { sharedWith: "${user.userId}" },
112
+ },
113
+ ],
114
+ },
115
+ billing_admin: {
116
+ role: "billing_admin",
117
+ inherits: "viewer",
118
+ description: "Manage billing and subscription",
119
+ rules: [
120
+ {
121
+ action: ["create", "read", "update"],
122
+ resource: "Billing",
123
+ },
124
+ {
125
+ action: ["read"],
126
+ resource: "Workspace",
127
+ },
128
+ ],
129
+ },
130
+ guest: {
131
+ role: "guest",
132
+ description: "Limited read access to specific shared resources only",
133
+ rules: [
134
+ {
135
+ action: ["read"],
136
+ resource: ["Document", "Project"],
137
+ conditions: { sharedWith: "${user.userId}", guestAccess: true },
138
+ },
139
+ ],
140
+ },
141
+ };
142
+ export function getDefaultRoles() {
143
+ return Object.values(DEFAULT_ROLES);
144
+ }
145
+ export function getRoleDefinition(role) {
146
+ return DEFAULT_ROLES[role];
147
+ }
148
+ export function getRoleHierarchy(role) {
149
+ const hierarchy = [role];
150
+ const definition = getRoleDefinition(role);
151
+ if (definition?.inherits) {
152
+ const inherits = Array.isArray(definition.inherits)
153
+ ? definition.inherits
154
+ : [definition.inherits];
155
+ for (const inherited of inherits) {
156
+ hierarchy.push(...getRoleHierarchy(inherited));
157
+ }
158
+ }
159
+ return [...new Set(hierarchy)];
160
+ }
@@ -0,0 +1,48 @@
1
+ export type Action = "create" | "read" | "update" | "delete" | "manage" | "invite" | "publish" | "export" | "share" | string;
2
+ export type Resource = "Document" | "Project" | "Workspace" | "User" | "Billing" | "ApiKey" | "Team" | "Invite" | "Integration" | string;
3
+ export type Role = "owner" | "admin" | "member" | "viewer" | "billing_admin" | "guest" | string;
4
+ export interface PermissionRule {
5
+ action: Action | Action[];
6
+ resource: Resource | Resource[];
7
+ conditions?: Record<string, unknown>;
8
+ inverted?: boolean;
9
+ reason?: string;
10
+ fields?: string[];
11
+ }
12
+ export interface RoleDefinition {
13
+ role: Role;
14
+ inherits?: Role | Role[];
15
+ rules: PermissionRule[];
16
+ description?: string;
17
+ }
18
+ export interface PermissionContext {
19
+ userId: string;
20
+ tenantId: string;
21
+ roles: Role[];
22
+ attributes?: Record<string, unknown>;
23
+ }
24
+ export interface PermissionProvider {
25
+ can(context: PermissionContext, action: Action, resource: Resource, subject?: unknown, field?: string): boolean;
26
+ cannot(context: PermissionContext, action: Action, resource: Resource, subject?: unknown, field?: string): boolean;
27
+ getRulesForRole(role: Role): PermissionRule[];
28
+ defineRole(definition: RoleDefinition): void;
29
+ buildAbilityFor(context: PermissionContext): unknown;
30
+ }
31
+ export type PermissionProviderType = "casl" | "openfga";
32
+ export interface PermissionConfig {
33
+ provider?: PermissionProviderType;
34
+ roles?: RoleDefinition[];
35
+ openFgaApiUrl?: string;
36
+ openFgaStoreId?: string | undefined;
37
+ openFgaAuthToken?: string | undefined;
38
+ }
39
+ export interface AbilityOptions {
40
+ resolveAction?: boolean;
41
+ }
42
+ export declare class ForbiddenError extends Error {
43
+ action: Action;
44
+ resource: Resource;
45
+ context?: PermissionContext | undefined;
46
+ constructor(action: Action, resource: Resource, context?: PermissionContext | undefined);
47
+ }
48
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GACd,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,OAAO,GACP,MAAM,CAAC;AAEX,MAAM,MAAM,QAAQ,GAChB,UAAU,GACV,SAAS,GACT,WAAW,GACX,MAAM,GACN,SAAS,GACT,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,aAAa,GACb,MAAM,CAAC;AAEX,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,eAAe,GAAG,OAAO,GAAG,MAAM,CAAC;AAEhG,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,QAAQ,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,EAAE,CAAC;IACzB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,CACD,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC;IACX,MAAM,CACJ,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC;IACX,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9C,UAAU,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI,CAAC;IAC7C,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC;CACtD;AAED,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,SAAS,CAAC;AAExD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,qBAAa,cAAe,SAAQ,KAAK;IAE9B,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,QAAQ;IAClB,OAAO,CAAC,EAAE,iBAAiB;gBAF3B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,iBAAiB,YAAA;CAKrC"}
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ export class ForbiddenError extends Error {
2
+ action;
3
+ resource;
4
+ context;
5
+ constructor(action, resource, context) {
6
+ super(`Forbidden: cannot ${action} on ${resource}`);
7
+ this.action = action;
8
+ this.resource = resource;
9
+ this.context = context;
10
+ this.name = "ForbiddenError";
11
+ }
12
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@nebutra/permissions",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
- "license": "AGPL-3.0",
5
+ "license": "MIT",
6
6
  "type": "module",
7
7
  "nebutra": {
8
8
  "status": "foundation",
@@ -12,7 +12,7 @@
12
12
  "or OPENFGA_API_URL + OPENFGA_STORE_ID for Zanzibar-style"
13
13
  ],
14
14
  "gaps": [
15
- "OpenFGA adapter is a stub"
15
+ "OpenFGA model and tuple lifecycle management remain external"
16
16
  ],
17
17
  "featureId": "permissions",
18
18
  "category": "iam",
@@ -54,7 +54,7 @@
54
54
  "@casl/ability": "^6.7.0",
55
55
  "@casl/prisma": "^1.5.1",
56
56
  "zod": "^4.3.6",
57
- "@nebutra/logger": "0.1.0"
57
+ "@nebutra/logger": "0.1.1"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "react": "^18.0.0 || ^19.0.0",