TypeDrop

2026-06-16 Challenge

2026-06-16 Hard

Typed Permission Policy Engine with Role Inheritance

You're building the authorization layer for a multi-tenant SaaS platform. Raw policy definitions arrive as `unknown` from a configuration service; your engine must validate them, resolve role inheritance chains, evaluate typed permission checks against a request context, and produce a strongly-typed authorization decision — with zero `any`.

Goals

  • Implement brand constructors (`toRoleId`, `toResourceId`, `toActionId`) that validate inputs and return `Result` types.
  • Implement `parsePolicy` to fully validate an `unknown` config payload into a strongly-typed `Policy`, returning structured `PolicyError` values for every violation.
  • Implement `resolveRole` to recursively expand role inheritance, merge permissions in the correct order, and detect cycles — all without `any`.
  • Implement `evaluateRequest` and the `authorize` convenience function, threading `PolicyResult<T>` through every step with exhaustive error handling.
challenge.ts
// Key types and main function signature preview

type RoleId     = string & { readonly __brand: "RoleId" };
type ResourceId = string & { readonly __brand: "ResourceId" };
type ActionId   = string & { readonly __brand: "ActionId" };

type Result<T, E> =
  | { readonly ok: true;  readonly value: T }
  | { readonly ok: false; readonly error: E };

type PolicyError =
  | { readonly kind: "INVALID_INPUT";    readonly message: string }
  | { readonly kind: "UNKNOWN_ROLE";     readonly roleId: RoleId }
  | { readonly kind: "CYCLE_DETECTED";   readonly cycle: RoleId[] }
  | { readonly kind: "UNKNOWN_RESOURCE"; readonly resourceId: ResourceId }
  | { readonly kind: "UNKNOWN_ACTION";   readonly actionId: ActionId };

type Decision =
  | { readonly verdict: "ALLOW"; readonly grantedBy: RoleId }
  | { readonly verdict: "DENY";  readonly deniedBy:  RoleId }
  | { readonly verdict: "NO_MATCH" };

// One-shot convenience: parse raw policy → resolve role → evaluate
declare function authorize(
  rawPolicy:   unknown,
  rawRoleId:   string,
  rawResource: string,
  rawAction:   string,
): Result<Decision, PolicyError>;
Hints (click to reveal)

Hints

  • Branded types can't be used as index signatures directly — use `{ [K in RoleId]: ... }` (a mapped type) rather than `{ [key: RoleId]: ... }` (an index signature).
  • For cycle detection in `resolveRole`, thread a `ReadonlySet<RoleId>` of ancestors through the recursive calls — if you encounter a role already in the set, you've found a cycle.
  • In `parsePolicy`, narrow `unknown` step-by-step: check `typeof`, then `Array.isArray`, then field-by-field — TypeScript will track the narrowing and let you avoid `as` casts.

Or clone locally

git clone -b challenge/2026-06-16 https://github.com/niltonheck/typedrop.git