endpoint-permissions-kit 0.1.0 → 0.2.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.
@@ -0,0 +1,17 @@
1
+ import type { Data, Properties } from './types';
2
+ declare const properties: {
3
+ /**
4
+ * Checks one path declared in `registerActions`.
5
+ * Array indexes never appear in the paths of `data` (elements share their
6
+ * container's path), and a leading wildcard would grant that leaf under every
7
+ * root key.
8
+ */
9
+ checkDeclaredPath(property: string, permissionPath: string): void;
10
+ /**
11
+ * Compares the paths of `data` with the fields the permission allows.
12
+ * Denies the request with `PROPERTIES_NOT_ALLOWED`, or returns a cropped copy
13
+ * when the `cropper` option is on. `data` is never mutated.
14
+ */
15
+ resolve(data: Data, allowed: Properties, cropper: boolean): Data;
16
+ };
17
+ export default properties;
@@ -16,4 +16,7 @@ export interface RoleBuilder {
16
16
  export interface GrantBuilder {
17
17
  registerActions(actions: GrantDefs): GrantBuilder;
18
18
  }
19
- export declare function defineModule(segment: string): ModuleBuilder;
19
+ declare const registry: {
20
+ defineModule(segment: string): ModuleBuilder;
21
+ };
22
+ export default registry;
@@ -1,13 +1,57 @@
1
- import type { Authorization, Method, PermissionId, Properties } from './types';
2
- import type { NameEntry } from './state';
3
- export type Access = {
1
+ import type { ModuleEntry, NameEntry, Snapshot } from './state';
2
+ import type { Method, MethodAccessMap, Properties, Role, UserAssignments } from './types';
3
+ /** The caller, with every assignment already checked against the sealed catalog. */
4
+ interface Identity {
5
+ readonly role: Role;
6
+ readonly assigned: ReadonlySet<string>;
7
+ readonly names: ReadonlyMap<string, string>;
8
+ }
9
+ /** The single permission of a module an identity resolves to. */
10
+ interface ChosenPermission {
11
+ readonly permissionId: string;
12
+ readonly entry: NameEntry;
13
+ }
14
+ type Access = {
4
15
  readonly status: 'granted';
5
16
  readonly properties: Properties;
6
- readonly authorization: Authorization;
7
17
  } | {
8
18
  readonly status: 'disabled';
9
19
  } | {
10
20
  readonly status: 'unassigned';
11
21
  };
12
- export declare function permissionIdOf(role: string, action: string, name: string): PermissionId;
13
- export declare function resolveAccess(nameEntry: NameEntry, permissionId: string, role: string, method: Method, assigned: ReadonlySet<string>): Access;
22
+ declare const resolve: {
23
+ /**
24
+ * Checks the identity a request or a view arrives with. Every assignment must be
25
+ * well formed, belong to the authenticated role and exist as assignable: one
26
+ * stale row denies everything, so it cannot silently widen or narrow access.
27
+ *
28
+ * It also indexes the assignments by module. A request names no permission name,
29
+ * so two names of the same module would leave the choice to the library: that is
30
+ * `AMBIGUOUS_PERMISSION` and it denies everything.
31
+ */
32
+ identity(assignments: UserAssignments, snapshot: Snapshot, roles: ReadonlySet<string>): Identity;
33
+ /**
34
+ * The permission of a module this identity resolves to. The request carries no
35
+ * name, so a direct assignment on that module decides, and without one the only
36
+ * name its grants reach does. `undefined` means the module is out of reach.
37
+ *
38
+ * Two reachable names deny with `AMBIGUOUS_PERMISSION`: picking one would widen
39
+ * or narrow the access the user was given, without anyone asking for it.
40
+ */
41
+ permission(registeredModule: ModuleEntry, action: string, identity: Identity): ChosenPermission | undefined;
42
+ /**
43
+ * Decides one `(role, module, name, method)`. A direct assignment decides
44
+ * completely and ignores grants; without it the applicable grants are combined.
45
+ */
46
+ access(nameEntry: NameEntry, permissionId: string, role: string, method: Method, assigned: ReadonlySet<string>): Access;
47
+ /**
48
+ * The four methods of one permission, for the read-only views.
49
+ * Returns `undefined` when the permission is out of reach, which is how a view
50
+ * distinguishes "not yours" from "yours but nothing enabled".
51
+ *
52
+ * Mirrors the precedence of `access` in a single pass over the grants, because a
53
+ * view needs no fields. Any change to `access` belongs here too.
54
+ */
55
+ methodAccess(nameEntry: NameEntry, permissionId: string, role: string, assigned: ReadonlySet<string>): MethodAccessMap | undefined;
56
+ };
57
+ export default resolve;
@@ -1 +1,8 @@
1
- export declare function seal(): void;
1
+ declare const sealer: {
2
+ /**
3
+ * Closes the registry: checks the references that could still be completed by
4
+ * a later import, then materializes the views. Idempotent.
5
+ */
6
+ seal(): void;
7
+ };
8
+ export default sealer;
@@ -1,10 +1,6 @@
1
+ import type { PermissionReference } from './identifiers';
1
2
  import type { ActionDefs, GrantDefs, HookFn, Method, NamedPermissionCatalog } from './types';
2
- export interface PermissionReference {
3
- readonly role: string;
4
- readonly action: string;
5
- readonly name: string;
6
- }
7
- export interface GrantEntry {
3
+ interface GrantEntry {
8
4
  readonly source: PermissionReference;
9
5
  readonly actions: GrantDefs;
10
6
  }
@@ -17,14 +13,21 @@ export interface ModuleEntry {
17
13
  readonly names: Map<string, NameEntry>;
18
14
  readonly hooks: Map<Method, HookFn[]>;
19
15
  }
20
- interface Snapshot {
16
+ export interface Snapshot {
21
17
  readonly named: NamedPermissionCatalog;
22
18
  readonly assignable: ReadonlySet<string>;
23
19
  }
24
20
  export interface State {
25
21
  roles: Set<string>;
22
+ cropper: boolean;
26
23
  readonly modules: Map<string, ModuleEntry>;
27
24
  snapshot: Snapshot | null;
28
25
  }
29
- export declare function getOrCreateState(): State;
30
- export {};
26
+ declare const state: {
27
+ getOrCreate(): State;
28
+ /** Every write to the registry goes through this: sealing freezes the configuration. */
29
+ requireOpen(currentState: State): void;
30
+ /** Every read of a materialized view goes through this. */
31
+ requireSnapshot(currentState: State): Snapshot;
32
+ };
33
+ export default state;
@@ -1,10 +1,10 @@
1
- import type { ALL_FIELDS, METHODS } from './constants';
1
+ import type constants from './constants';
2
2
  export interface RoleRegistry {
3
3
  general: true;
4
4
  }
5
5
  export type Role = keyof RoleRegistry & string;
6
- export type Method = (typeof METHODS)[number];
7
- export type Properties = readonly string[] | typeof ALL_FIELDS;
6
+ export type Method = (typeof constants.METHODS)[number];
7
+ export type Properties = readonly string[] | typeof constants.ALL_FIELDS;
8
8
  export type PermissionId = `${Role}::${string}::${string}`;
9
9
  export interface ActionDef {
10
10
  enabled: boolean;
@@ -18,21 +18,12 @@ export interface GrantDef {
18
18
  export type GrantDefs = Partial<Record<Method, GrantDef>>;
19
19
  export type Data = Record<string, unknown>;
20
20
  export type Context = Record<string, unknown>;
21
- export interface Authorization {
22
- readonly direct: boolean;
23
- readonly grantedBy: readonly PermissionId[];
24
- }
25
- export interface ResolvedPermission {
26
- readonly role: Role;
27
- readonly action: string;
28
- readonly name: string;
29
- readonly permissionId: PermissionId;
30
- readonly method: Method;
31
- readonly enabled: true;
32
- readonly properties: Properties;
33
- readonly authorization: Authorization;
34
- }
35
- export type HookFn = (data: Data | undefined, context: Context | undefined, permission: ResolvedPermission) => unknown;
21
+ export type ContextValues = {
22
+ roles: readonly string[];
23
+ cropper: boolean;
24
+ };
25
+ export type ContextKey = keyof ContextValues;
26
+ export type HookFn = (data: Data, context: Context, permissions: readonly string[]) => unknown;
36
27
  export type ValidationError = {
37
28
  readonly code: Exclude<PkitErrorCode, 'PROPERTIES_NOT_ALLOWED'>;
38
29
  readonly message: string;
@@ -46,29 +37,30 @@ export type ValidationError = {
46
37
  readonly cause: unknown;
47
38
  };
48
39
  export type ValidationErrorCode = ValidationError['code'];
49
- export type PkitErrorCode = 'ROLE_NOT_DECLARED' | 'DUPLICATE_REGISTRATION' | 'INVALID_DEFINITION' | 'INVALID_INPUT' | 'SEALED' | 'NOT_SEALED' | 'UNKNOWN_ROLE' | 'UNKNOWN_ACTION' | 'UNKNOWN_PERMISSION' | 'PERMISSION_ROLE_MISMATCH' | 'PERMISSION_NOT_ASSIGNED' | 'METHOD_DISABLED' | 'PROPERTIES_NOT_ALLOWED';
40
+ export type PkitError = Error & {
41
+ name: 'PkitError';
42
+ } & ({
43
+ code: Exclude<PkitErrorCode, 'PROPERTIES_NOT_ALLOWED'>;
44
+ } | {
45
+ code: 'PROPERTIES_NOT_ALLOWED';
46
+ fields: readonly string[];
47
+ });
48
+ export type PkitErrorCode = 'ROLE_NOT_DECLARED' | 'DUPLICATE_REGISTRATION' | 'INVALID_DEFINITION' | 'INVALID_INPUT' | 'SEALED' | 'NOT_SEALED' | 'UNKNOWN_ROLE' | 'UNKNOWN_ACTION' | 'UNKNOWN_PERMISSION' | 'AMBIGUOUS_PERMISSION' | 'PERMISSION_ROLE_MISMATCH' | 'PERMISSION_NOT_ASSIGNED' | 'METHOD_DISABLED' | 'PROPERTIES_NOT_ALLOWED';
50
49
  export interface UserAssignments {
51
50
  role: Role;
52
51
  permissions: readonly string[];
53
52
  }
54
- interface PermissionInput extends UserAssignments {
53
+ export interface ValidateInput extends UserAssignments {
55
54
  action: string;
56
- name: string;
57
- context?: Context;
58
- }
59
- export interface FindInput extends PermissionInput {
60
- method: 'find';
61
- select?: readonly string[];
55
+ method: Method;
62
56
  data?: Data;
57
+ context?: Context;
63
58
  }
64
- export interface WriteInput<RequestData extends Data = Data> extends PermissionInput {
65
- method: Exclude<Method, 'find'>;
66
- data: RequestData;
59
+ export interface ValidateData {
60
+ readonly data: Data;
67
61
  }
68
- export type ValidateInput = FindInput | WriteInput;
69
- export type FindResult = Properties;
70
- export type ValidateResult<Result> = {
71
- readonly result: Result;
62
+ export type ValidateResult = {
63
+ readonly result: ValidateData;
72
64
  readonly errors: readonly [];
73
65
  } | {
74
66
  readonly result: null;
@@ -78,4 +70,3 @@ export type PermissionEntry = Readonly<ActionDef>;
78
70
  export type NamedPermissionCatalog = Readonly<Record<PermissionId, Readonly<Partial<Record<Method, PermissionEntry>>>>>;
79
71
  export type MethodAccessMap = Readonly<Record<Method, boolean>>;
80
72
  export type UserPermissionMap = Readonly<Record<PermissionId, MethodAccessMap>>;
81
- export {};
@@ -1,3 +1,10 @@
1
- import type { Data, FindInput, FindResult, ValidateResult, WriteInput } from './types';
2
- export declare function validate(input: FindInput): Promise<ValidateResult<FindResult>>;
3
- export declare function validate<RequestData extends Data>(input: WriteInput<RequestData>): Promise<ValidateResult<RequestData>>;
1
+ import type { ValidateInput, ValidateResult } from './types';
2
+ declare const validator: {
3
+ /**
4
+ * The request boundary. Every failure of every phase becomes one entry of the
5
+ * returned contract, so this never throws and never answers with a partial
6
+ * success: on any failure `result` is `null`.
7
+ */
8
+ validate(input: ValidateInput): Promise<ValidateResult>;
9
+ };
10
+ export default validator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "endpoint-permissions-kit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Framework-agnostic endpoint permissions with an in-memory registry, typed roles and validation hooks",
5
5
  "license": "Apache-2.0",
6
6
  "author": "yellyoshua",
@@ -1,53 +0,0 @@
1
- import type { GrantEntry, ModuleEntry, NameEntry, PermissionReference, State } from './state';
2
- import type { ActionDefs, Context, Data, FindResult, HookFn, Method, PkitErrorCode, ResolvedPermission, Role, UserAssignments, ValidateInput } from './types';
3
- interface ValidationFailure {
4
- code: PkitErrorCode;
5
- message: string;
6
- }
7
- interface StringRules extends ValidationFailure {
8
- minimumLength: number;
9
- }
10
- interface DirectRegistration {
11
- action: string;
12
- name: string;
13
- role: string;
14
- }
15
- interface GrantRegistration {
16
- action: string;
17
- name: string;
18
- permissionId: string;
19
- }
20
- interface HookRegistration {
21
- action: string;
22
- name?: string;
23
- role?: string;
24
- method: Method;
25
- }
26
- interface Identity {
27
- role: Role;
28
- assigned: ReadonlySet<string>;
29
- }
30
- interface ValidatedRequest {
31
- registeredModule: ModuleEntry;
32
- nameEntry: NameEntry;
33
- permission: ResolvedPermission;
34
- data: Data | undefined;
35
- context: Context | undefined;
36
- result: FindResult | Data;
37
- }
38
- export declare function validateStrings(value: unknown, rules: StringRules): asserts value is readonly string[];
39
- export declare function validateContextKey(key: unknown): asserts key is 'roles';
40
- export declare function validateSnapshot<Snapshot>(snapshot: Snapshot | null): asserts snapshot is Snapshot;
41
- export declare function validateRole(role: unknown, roles: ReadonlySet<string>, code: 'ROLE_NOT_DECLARED' | 'UNKNOWN_ROLE'): asserts role is Role;
42
- export declare function validateRoleCatalog(roles: unknown, state: State): asserts roles is readonly string[];
43
- export declare function validateRoleSegment(role: unknown): asserts role is string;
44
- export declare function validateModuleName(moduleName: unknown): asserts moduleName is string;
45
- export declare function validatePermissionName(name: unknown): asserts name is string;
46
- export declare function parsePermissionId(value: unknown, failure: ValidationFailure): PermissionReference;
47
- export declare function validateActions(actions: unknown, registration: DirectRegistration, state: State): ActionDefs;
48
- export declare function validateGrantActions(actions: unknown, registration: GrantRegistration, state: State): GrantEntry;
49
- export declare function validateHook(hook: unknown, registration: HookRegistration, state: State): asserts hook is HookFn;
50
- export declare function validateSealedRegistry(modules: ReadonlyMap<string, ModuleEntry>): void;
51
- export declare function validateIdentity(assignments: UserAssignments, state: State): Identity;
52
- export declare function validateRequest(input: ValidateInput, state: State): ValidatedRequest;
53
- export {};