first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
# condition-matcher
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the condition evaluation engine for ABAC (Attribute-Based Access Control) in `@suaveplan/permissions`. `DefaultConditionMatcher` evaluates `PermissionCondition` objects against a request context, enabling fine-grained access control rules like "user can read posts only if post.authorId === userId".
The `compareValues` function is the pure functional core — it handles all 12 supported operators and is used directly by both `DefaultConditionMatcher` and the `Ability` class for rule evaluation.
## Features
- `compareValues(a, b, operator)` — pure comparison function supporting all 12 `ConditionOperator` values
- `DefaultConditionMatcher` implementing `ConditionMatcher` interface
- Dot-notation field resolution from context objects (e.g. `"post.author.id"`)
- Safe `RegExp` evaluation for `matches` operator (catches invalid patterns)
- `ConditionMatcher` interface for custom implementations
## Usage: Basic Example
```ts
import { DefaultConditionMatcher, compareValues } from "@suaveplan/permissions";
const matcher = new DefaultConditionMatcher();
const result = matcher.matches(
{ field: "user.role", operator: "eq", value: "admin" },
{ user: { role: "admin" } },
);
console.log(result); // true
```
## Usage: Advanced Example
```ts
import { compareValues } from "@suaveplan/permissions";
// Numeric comparison
compareValues(5, 3, "gt"); // true
compareValues(2, 5, "lt"); // true
// Array membership
compareValues("admin", ["admin", "editor"], "in"); // true
compareValues("viewer", ["admin", "editor"], "nin"); // true
// String operations
compareValues("hello world", "hello", "startsWith"); // true
compareValues("test@example.com", "@example", "contains"); // true
compareValues("abc123", "^[a-z]+\\d+$", "matches"); // true
```
## API Reference
### `compareValues(a, b, operator): boolean`
Pure comparison. Both `a` (actual) and `b` (expected) are `unknown`. Returns `false` for type mismatches.
Operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `startsWith`, `endsWith`, `matches`.
### `ConditionMatcher`
Interface: `matches(condition: PermissionCondition, context: Record<string, unknown>): boolean`.
### `DefaultConditionMatcher`
Implements `ConditionMatcher`. Resolves `condition.field` from `context` using dot notation before calling `compareValues`.
## Implementation Notes
Field resolution uses `.split(".")` iteration rather than `eval` or `Function()` — it is safe from injection attacks and works with any serialisable context object. The `matches` operator wraps `RegExp` construction in a `try/catch` to prevent invalid patterns from throwing at evaluation time. Numeric comparison operators (`gt`, `gte`, `lt`, `lte`) return `false` if either operand is not a `number`, rather than attempting type coercion, to prevent surprising behaviour with string numbers.

View File

@@ -0,0 +1,64 @@
# field-matcher
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the `FieldMatcher` interface and its default implementation for resolving field values from request context objects in `@suaveplan/permissions`. Separating field resolution from condition evaluation allows custom implementations to support advanced context shapes such as flat maps, typed records, or proxy objects that intercept property access.
The `fieldMatches` convenience function combines field resolution with condition evaluation in a single call, making it easy to check individual conditions without instantiating a full `ConditionMatcher`.
## Features
- `FieldMatcher` interface with a single `get(context, field): unknown` method
- `DefaultFieldMatcher` implementing dot-notation path resolution
- `fieldMatches(context, condition, matcher?)` convenience combinator
- Handles `null` and `undefined` intermediate values gracefully
## Usage: Basic Example
```ts
import { DefaultFieldMatcher, fieldMatches } from "@suaveplan/permissions";
const context = { post: { authorId: "u1" } };
const condition = { field: "post.authorId", operator: "eq", value: "u1" };
console.log(fieldMatches(context, condition)); // true
```
## Usage: Advanced Example
```ts
import type { FieldMatcher } from "@suaveplan/permissions";
class PrefixedFieldMatcher implements FieldMatcher {
constructor(private readonly prefix: string) {}
get(context: Record<string, unknown>, field: string): unknown {
return context[`${this.prefix}.${field}`];
}
}
const matcher = new PrefixedFieldMatcher("request");
const context = { "request.userId": "u42" };
const val = matcher.get(context, "userId");
console.log(val); // "u42"
```
## API Reference
### `FieldMatcher`
Interface: `get(context: Record<string, unknown>, field: string): unknown`.
### `DefaultFieldMatcher`
Implements `FieldMatcher`. Resolves dot-notation paths (e.g. `"a.b.c"`) by iterating path segments. Returns `undefined` if any intermediate value is `null`, `undefined`, or non-object.
### `fieldMatches(context, condition, matcher?): boolean`
Resolves `condition.field` from `context` using `matcher` (default: `DefaultFieldMatcher`), then calls `compareValues`. Returns `false` if the field is not found.
## Implementation Notes
The `DefaultFieldMatcher.get` method uses the same safe dot-notation traversal as `DefaultConditionMatcher` — both iterate segments without `eval`. The `fieldMatches` function creates a new `DefaultFieldMatcher` instance if none is provided; for hot paths where this function is called millions of times, pass a pre-constructed matcher to avoid the per-call allocation.

View File

@@ -0,0 +1,60 @@
# subject-detector
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the `SubjectDetector` interface and its default implementation for extracting a resource type string from an arbitrary object in `@suaveplan/permissions`. Subject detection enables the `Ability` system to determine which permission rules apply to a given resource instance at runtime, without requiring the caller to manually pass a resource type string on every check.
The default implementation looks for well-known properties (`__typename`, `type`, `resourceType`) that are commonly present in GraphQL responses, domain objects, and REST API payloads.
## Features
- `SubjectDetector` interface with a single `detect(obj): string | undefined` method
- `DefaultSubjectDetector` checking `__typename`, `type`, and `resourceType` properties
- Safe: handles `null`, `undefined`, and non-object values without throwing
- Returns `undefined` (not throws) when no subject type can be determined
## Usage: Basic Example
```ts
import { DefaultSubjectDetector } from "@suaveplan/permissions";
const detector = new DefaultSubjectDetector();
const post = { __typename: "Post", id: "1", title: "Hello" };
console.log(detector.detect(post)); // "Post"
const untyped = { id: "1", value: 42 };
console.log(detector.detect(untyped)); // undefined
```
## Usage: Advanced Example
```ts
import type { SubjectDetector } from "@suaveplan/permissions";
class DomainSubjectDetector implements SubjectDetector {
detect(obj: unknown): string | undefined {
if (typeof obj !== "object" || obj === null) return undefined;
const r = obj as Record<string, unknown>;
// Domain objects use a Symbol-keyed type tag
return typeof r["entityType"] === "string" ? r["entityType"] : undefined;
}
}
```
## API Reference
### `SubjectDetector`
Interface: `detect(obj: unknown): string | undefined`.
### `DefaultSubjectDetector`
Inspects `obj.__typename`, `obj.type`, and `obj.resourceType` in that order, returning the first non-empty string value found, or `undefined`.
## Implementation Notes
The property lookup order (`__typename` first) is deliberate: it prioritises GraphQL-style typing, which is the most common source of typed resource objects in modern APIs. The check is purely structural (duck-typing) — no class hierarchy or prototype inspection is performed, ensuring broad compatibility with plain objects, frozen records, and proxy-wrapped values.