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.
Useful resources
Or clone locally
git clone -b challenge/2026-06-16 https://github.com/niltonheck/typedrop.git