TypeDrop
2026-04-24 Challenge
2026-04-24
Hard
Typed Permission Policy Evaluator
You're building the authorization engine for a multi-tenant SaaS platform. Raw policy documents arrive as `unknown` JSON from an admin dashboard; your engine must validate them, compile each rule into a strongly-typed decision graph, evaluate access requests against matching policies using precedence logic, and emit a discriminated-union verdict per request — with zero `any`.
Goals
- Implement `parsePolicy` to validate raw `unknown` JSON into a fully branded, strongly-typed `Policy` using a fail-fast `Result<T, E>` monad — no `any`, no type assertions.
- Implement `evaluateCondition` with an exhaustive discriminated-union switch that returns a typed `ConditionFailure` or `null`, covering all four condition kinds.
- Implement `evaluate` to fetch applicable policies from the typed `PolicyStore`, select the single highest-priority matching rule, evaluate its conditions, and return the correct discriminated-union `Verdict`.
- Implement `formatVerdict` with an exhaustive switch (guarded by a `never` check) that produces human-readable output for all four `Verdict` variants.
challenge.ts
// Key types & main evaluator signature — challenge preview
type Verdict =
| { readonly decision: "allow"; readonly matchedRule: RuleMatch }
| { readonly decision: "deny"; readonly matchedRule: RuleMatch }
| { readonly decision: "deny-no-match" }
| { readonly decision: "deny-condition"; readonly matchedRule: RuleMatch;
readonly failures: ReadonlyArray<ConditionFailure> };
type RuleMatch = {
readonly policyId: PolicyId;
readonly ruleId: string;
readonly effect: "allow" | "deny";
readonly priority: number;
};
// Parse raw unknown JSON into a validated, branded Policy
function parsePolicy(raw: unknown): Result<Policy, ValidationError> { … }
// Evaluate a single access request against all applicable policies
function evaluate(ctx: RequestContext, store: IPolicyStore): Verdict { … }
// Evaluate many requests, returning parallel results
function evaluateBatch(
requests: ReadonlyArray<RequestContext>,
store: IPolicyStore,
): ReadonlyArray<BatchResult> { … }
Hints (click to reveal)
Hints
- For branded types without `any` or `as`, look into using a generic identity function constrained to the brand shape — `function brand<B extends Brand<string, string>>(s: string): B` won't compile; think about what *does* work at the call site under `strict: true`.
- Your `evaluateCondition` switch must handle every `Condition` kind — add a `default: (c: never) => ...` branch and TypeScript will tell you if you've missed one.
- In `evaluate`, collect *all* condition failures for the top rule before deciding — don't short-circuit on the first failure, so the `deny-condition` verdict includes the full `failures` array.
Useful resources
Or clone locally
git clone -b challenge/2026-04-24 https://github.com/niltonheck/typedrop.git