{"_id":"@aegis-runtime/aegisauth","name":"@aegis-runtime/aegisauth","dist-tags":{"latest":"0.2.0"},"versions":{"0.2.0":{"name":"@aegis-runtime/aegisauth","version":"0.2.0","description":"Type-safe, explainable policy-as-code authorization engine with static route analysis for Node/TypeScript.","license":"MIT","type":"module","main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"},"./express":{"import":"./dist/express.js","types":"./dist/express.d.ts"},"./shadow":{"import":"./dist/shadow.js","types":"./dist/shadow.d.ts"},"./otel":{"import":"./dist/otel.js","types":"./dist/otel.d.ts"}},"sideEffects":false,"bin":{"aegisauth":"bin/aegisauth.cjs"},"scripts":{"build":"tsc -p tsconfig.json","test":"vitest","prepare":"npm run build","clean":"rm -rf dist"},"dependencies":{"typescript":"^5.5.0"},"devDependencies":{"@types/express":"^4.17.21","@types/node":"^22.0.0","express":"^4.19.2","vitest":"^3.2.4","@opentelemetry/api":"^1.9.0"},"peerDependencies":{"express":"^4.17.0 || ^5.0.0","@opentelemetry/api":"^1.9.0"},"engines":{"node":">=18.0.0"},"repository":{"type":"git","url":"git+https://github.com/aegis-runtime/aegisauth.git"},"bugs":{"url":"https://github.com/aegis-runtime/aegisauth/issues"},"homepage":"https://github.com/aegis-runtime/aegisauth#readme","keywords":["authorization","access-control","policy-as-code","typescript","express","security","open-telemetry","observability","shadow-testing"],"_id":"@aegis-runtime/aegisauth@0.2.0","_nodeVersion":"24.11.1","_npmVersion":"11.6.2","dist":{"integrity":"sha512-AHruwgwgpnTPqLDhpFUhr3BWH0EskzWrihpIdsd82uIKBknNruUTZjcGeHqfvuIcV6gwzQBOSMwon2bZ59Tj/A==","shasum":"b115d44a1123748bd47505ae5aa690ec18c880a5","tarball":"https://registry.npmjs.org/@aegis-runtime/aegisauth/-/aegisauth-0.2.0.tgz","fileCount":18,"unpackedSize":64865,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIC+K+1kFDIowp3SydRVEEIDRihLgQQeQnGdd6LzG2dI1AiEAnajNdy4ae99MsBQMh7AH4vZp8RZX1VT5sRlFgquP3QY="}]},"_npmUser":{"name":"tejassathe117","email":"tejassathe010@gmail.com"},"directories":{},"maintainers":[{"name":"tejassathe117","email":"tejassathe010@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/aegisauth_0.2.0_1768096113824_0.6520237758201908"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-11T01:48:33.737Z","0.2.0":"2026-01-11T01:48:33.957Z","modified":"2026-01-11T01:48:34.274Z"},"maintainers":[{"name":"tejassathe117","email":"tejassathe010@gmail.com"}],"description":"Type-safe, explainable policy-as-code authorization engine with static route analysis for Node/TypeScript.","homepage":"https://github.com/aegis-runtime/aegisauth#readme","keywords":["authorization","access-control","policy-as-code","typescript","express","security","open-telemetry","observability","shadow-testing"],"repository":{"type":"git","url":"git+https://github.com/aegis-runtime/aegisauth.git"},"bugs":{"url":"https://github.com/aegis-runtime/aegisauth/issues"},"license":"MIT","readme":"# AegisAuth\n\nType-safe, explainable **policy-as-code authorization** for TypeScript/Node, with a CLI that scans your routes and tells you which ones are missing authorization.\n\n- ✅ **Centralized policies** instead of scattered `if (user.role === 'admin')` checks  \n- ✅ **Explainable decisions**: every allow/deny comes with human-readable reasons  \n- ✅ **Type-safe DSL** for `ctx` and `resource` objects  \n- ✅ **Policy intelligence**: introspection APIs, role-based summaries, rule metadata  \n- ✅ **Express adapter**: `authorize(engine, 'invoice', 'read', resolve)`  \n- ✅ **Static analysis CLI**: `@aegis-runtime/aegisauth report src` (+ `--json`) to flag routes without `authorize()`  \n- ✅ **OpenTelemetry integration**: built-in observability for authorization decisions  \n- ✅ **Shadow testing**: safely test new policies alongside production policies  \n- ✅ No DB, queues, or external services required — pure TypeScript library  \n\n---\n\n## Table of Contents\n\n1. [Architecture](#architecture)  \n2. [Tech Stack](#tech-stack)  \n3. [Motivation](#motivation)  \n4. [Core Concepts](#core-concepts)  \n5. [Installation](#installation)  \n6. [Defining Policies](#defining-policies)  \n7. [Policy Intelligence & Introspection](#policy-intelligence--introspection)  \n8. [Making Decisions Manually](#making-decisions-manually)  \n9. [Using the Express Adapter](#using-the-express-adapter)  \n10. [Advanced Features](#advanced-features)  \n    - [OpenTelemetry Integration](#opentelemetry-integration)  \n    - [Shadow Testing](#shadow-testing)  \n11. [CLI: Route Authorization Report](#cli-route-authorization-report)  \n12. [Examples & Project Structure](#examples--project-structure)  \n13. [Design Notes](#design-notes)  \n14. [Roadmap](#roadmap)  \n15. [License](#license)  \n\n---\n\n## Architecture\n\nAegisAuth follows a layered architecture designed for flexibility, type safety, and observability:\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│                     Application Layer                        │\n│  (Express Routes, RPC Handlers, Background Jobs, etc.)      │\n└───────────────────────┬─────────────────────────────────────┘\n                        │\n                        │ authorize() middleware / engine.decide()\n                        │\n┌───────────────────────▼─────────────────────────────────────┐\n│                    Framework Adapters                        │\n│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │\n│  │   Express    │  │  OpenTelemetry│  │   Shadow     │      │\n│  │   Adapter    │  │   Integration │  │   Testing    │      │\n│  └──────────────┘  └──────────────┘  └──────────────┘      │\n└───────────────────────┬─────────────────────────────────────┘\n                        │\n                        │ PolicyEngine API\n                        │\n┌───────────────────────▼─────────────────────────────────────┐\n│                  Policy Engine Core                          │\n│  ┌──────────────────────────────────────────────────────┐   │\n│  │  Rule Storage (Map<resourceType, Map<action, Rule[]>>)│   │\n│  │  Decision Logic (deny-overrides semantics)            │   │\n│  │  Introspection APIs (listRules, findRules, etc.)      │   │\n│  │  Capability Snapshots                                  │   │\n│  └──────────────────────────────────────────────────────┘   │\n└───────────────────────┬─────────────────────────────────────┘\n                        │\n                        │ Type-safe DSL\n                        │\n┌───────────────────────▼─────────────────────────────────────┐\n│                  Policy Definitions                          │\n│  (Your domain-specific Ctx, Resources, and Rules)           │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### Key Architectural Principles\n\n1. **Separation of Concerns**: The core engine is framework-agnostic; adapters bridge to specific frameworks (Express, OpenTelemetry, etc.)\n\n2. **Type Safety**: Full TypeScript generics ensure compile-time type checking for contexts, resources, and actions\n\n3. **Extensibility**: Hook-based architecture (`onDecision`, `onDivergence`) enables observability and custom behaviors\n\n4. **Performance**: In-memory rule evaluation with O(1) lookups by resource type and action\n\n5. **Testability**: Pure functions, no side effects, and deterministic decision logic make unit testing straightforward\n\n### Module Structure\n\n```\n@aegis-runtime/aegisauth\n├── core (default export)\n│   └── PolicyEngine, createPolicyEngine, Decision, RuleMeta\n├── /express\n│   └── authorize, authorizeWithShadow, AuthorizeOptions\n├── /shadow\n│   └── createShadowEngine, ShadowEngine, ShadowDivergenceInfo\n└── /otel\n    └── createPolicyEngineWithOtel, AegisAuthOtelOptions\n```\n\n---\n\n## Tech Stack\n\n### Core Technologies\n\n- **TypeScript 5.5+**: Full type safety, modern language features, and excellent IDE support\n- **Node.js 18+**: ESM support, modern JavaScript runtime\n- **TypeScript Compiler API**: Used by CLI for static analysis of route definitions\n\n### Build & Testing\n\n- **TypeScript Compiler (tsc)**: Type checking and compilation\n- **Vitest**: Fast, modern testing framework\n- **ESM (ES Modules)**: Native module system support\n\n### Runtime Dependencies\n\n- **Zero runtime dependencies** for the core engine\n- **Peer dependencies**:\n  - `express` (^4.17.0 || ^5.0.0): For Express adapter\n  - `@opentelemetry/api` (^1.9.0): For OpenTelemetry integration\n\n### Developer Experience\n\n- **TypeScript strict mode**: Maximum type safety\n- **Declaration files (.d.ts)**: Full type information for consumers\n- **Tree-shakeable exports**: Modern bundlers can eliminate unused code\n- **Side-effect free**: Safe for tree-shaking and module optimization\n\n### Observability & Monitoring\n\n- **OpenTelemetry**: Standard observability protocol\n  - Metrics: Decision counts, latency histograms\n  - Traces: Spans for each authorization decision\n  - Attributes: Resource type, action, outcome, reasons\n\n---\n\n## Motivation\n\nAuthorization (authZ) answers the question: **\"What is this user allowed to do?\"**\n\nIn most Node/TypeScript backends, authorization logic:\n\n- Is scattered across controllers and services  \n- Is written as ad-hoc conditionals like `if (user.role === 'admin')` everywhere  \n- Has no single source of truth for \"who can do what\"  \n- Is hard to test, hard to audit, and hard to change safely  \n\n**AegisAuth** aims to fix this by providing:\n\n1. A **central policy engine** where you define all your rules as code  \n2. A **type-safe DSL** for expressing permissions with context and resources  \n3. **Explainable decisions**, with reasons returned for each allow/deny  \n4. An **Express adapter** to protect routes via middleware  \n5. A **CLI** that statically scans your code for routes that lack authorization  \n6. A **policy intelligence layer** to introspect and summarize your rules  \n7. **OpenTelemetry integration** for production observability  \n8. **Shadow testing** capabilities for safe policy migrations  \n\nThe goal is to treat authorization as **first-class, testable, auditable code** rather than scattered `if` statements.\n\n---\n\n## Core Concepts\n\nAegisAuth revolves around four core ideas:\n\n### 1. Context (`Ctx`)\n\nRepresents who is making the request and global request context.\n\n```ts\ntype Ctx = {\n  user: {\n    id: string;\n    orgId: string;\n    roles: string[];\n  } | null;\n};\n```\n\nYou decide the shape of `Ctx` for your application.\n\n### 2. Resources (`Resources`)\n\nRepresents the domain objects you protect, such as `Invoice`, `Project`, `Organization`, etc.\n\n```ts\ninterface Invoice {\n  id: string;\n  orgId: string;\n  status: 'draft' | 'paid';\n}\n\ninterface Resources {\n  invoice: Invoice;\n}\n```\n\nEach resource type is referenced by a string key (e.g. `'invoice'`).\n\n### 3. Policies\n\nPolicies answer **\"Who can perform which action on which resource, under which conditions?\"**\n\nYou express policies with a fluent, type-safe DSL:\n\n```ts\nengine\n  .forResource('invoice')\n  .can('read')\n    .when((ctx, invoice) => !!ctx.user && ctx.user.orgId === invoice.orgId)\n    .because('User belongs to same organization as the invoice')\n  .can('delete')\n    .when((ctx, invoice) => !!ctx.user && ctx.user.roles.includes('admin'))\n    .because('Admins can delete invoices in their org')\n  .cannot('delete')\n    .when((_ctx, invoice) => invoice.status === 'paid')\n    .because('Paid invoices cannot be deleted for compliance');\n```\n\nAt runtime, AegisAuth evaluates policies to produce a **Decision**:\n\n```ts\ninterface Decision {\n  allowed: boolean;\n  reasons: string[];\n  matchedRuleIds: string[];\n}\n```\n\n* `allowed` – final yes/no answer\n* `reasons` – human-readable explanations\n* `matchedRuleIds` – internal rule IDs (helpful for debugging and audits)\n\nAegisAuth uses **deny-overrides semantics**: any matching `cannot` rule will deny access, even if a `can` rule also matches.\n\n### 4. Rule Metadata & Policy Intelligence\n\nEach rule can carry structured metadata for analysis:\n\n```ts\nengine\n  .forResource('invoice')\n  .can('delete')\n    .when((ctx, invoice) =>\n      !!ctx.user &&\n      ctx.user.roles.includes('admin') &&\n      ctx.user.orgId === invoice.orgId\n    )\n    .because('Admins can delete invoices in their org', {\n      roles: ['admin'],\n      tags: ['invoice', 'delete'],\n      severity: 'high',\n      descriptionId: 'INV-DEL-001',\n    });\n```\n\nThis metadata drives the **introspection APIs** and lets you answer questions like:\n\n* \"What can `admin` do in this system?\"\n* \"Which rules are high-severity and related to invoices?\"\n\n---\n\n## Installation\n\nInstall from npm:\n\n```bash\nnpm install @aegis-runtime/aegisauth\n```\n\nIf you use the Express adapter, also install Express:\n\n```bash\nnpm install express\n```\n\nIf you use OpenTelemetry integration, also install:\n\n```bash\nnpm install @opentelemetry/api\n```\n\n> AegisAuth is ESM-only and targets Node 18+.\n> `express` and `@opentelemetry/api` are peer dependencies; the library does not bundle them.\n\n---\n\n## Defining Policies\n\nYou start by creating a policy engine with your own `Ctx` and `Resources` types.\n\n```ts\nimport { createPolicyEngine } from '@aegis-runtime/aegisauth';\n\ninterface User {\n  id: string;\n  orgId: string;\n  roles: string[];\n}\n\ninterface Invoice {\n  id: string;\n  orgId: string;\n  status: 'draft' | 'paid';\n}\n\ntype Ctx = { user: User | null };\n\ninterface Resources {\n  invoice: Invoice;\n}\n\nexport const engine = createPolicyEngine<Ctx, Resources>();\n\nengine\n  .forResource('invoice')\n  .can('read')\n    .when((ctx, invoice) => !!ctx.user && ctx.user.orgId === invoice.orgId)\n    .because('User belongs to same organization as the invoice', {\n      roles: ['user'],\n      tags: ['invoice', 'read'],\n    })\n  .can('delete')\n    .when((ctx, invoice) =>\n      !!ctx.user &&\n      ctx.user.roles.includes('admin') &&\n      ctx.user.orgId === invoice.orgId\n    )\n    .because('Admins can delete invoices in their org', {\n      roles: ['admin'],\n      tags: ['invoice', 'delete'],\n      severity: 'high',\n    })\n  .cannot('delete')\n    .when((_ctx, invoice) => invoice.status === 'paid')\n    .because('Paid invoices cannot be deleted for compliance', {\n      tags: ['invoice', 'delete', 'compliance'],\n      severity: 'high',\n    });\n```\n\n### API Overview\n\n* `createPolicyEngine<Ctx, Resources>()` – create an engine\n* `engine.forResource('invoice')` – start defining rules for a resource type\n* `.can(action)` / `.cannot(action)` – define allow/deny rules for an action\n* `.when((ctx, resource) => boolean)` – attach a condition (optional)\n* `.because(description, meta?)` – finalize the rule with a human-readable explanation and optional metadata\n\nIf you omit `.when(...)`, the rule is treated as **unconditional** (always true).\n\n---\n\n## Policy Intelligence & Introspection\n\nAegisAuth exposes introspection APIs so you can treat authorization as data, not just behavior.\n\n### Rule Metadata Type\n\n```ts\ninterface RuleMeta {\n  roles?: string[];\n  tags?: string[];\n  severity?: 'low' | 'medium' | 'high';\n  descriptionId?: string;\n  [key: string]: any;\n}\n```\n\n### Listing All Rules\n\n```ts\nconst rules = engine.listRules();\n/*\n[\n  {\n    id: 'invoice:read:1',\n    resourceType: 'invoice',\n    action: 'read',\n    effect: 'allow',\n    description: 'User belongs to same organization as the invoice',\n    meta: { roles: ['user'], tags: ['invoice', 'read'] }\n  },\n  ...\n]\n*/\n```\n\n### Finding Rules by Predicate\n\n```ts\nconst highRiskInvoiceRules = engine.findRules(\n  (r) =>\n    r.resourceType === 'invoice' &&\n    r.meta?.severity === 'high'\n);\n```\n\n### Summarizing by Role\n\n```ts\nconst adminSummary = engine.summarizeByRole('admin');\n\n/*\n[\n  {\n    role: 'admin',\n    resourceType: 'invoice',\n    action: 'delete',\n    effect: 'allow',\n    description: 'Admins can delete invoices in their org'\n  },\n  ...\n]\n*/\n```\n\n### Capability Snapshots\n\nGenerate capability matrices for batches of resources:\n\n```ts\nconst snapshot = engine.snapshotCapabilities({\n  ctx: { user: adminUser },\n  resources: {\n    invoice: [invoice1, invoice2, invoice3],\n  },\n  version: '1.0.0',\n});\n\n// snapshot.capabilities['invoice']['delete'] = [true, false, true]\n// → invoice1: can delete, invoice2: cannot, invoice3: can delete\n```\n\nYou can expose this internally as a JSON endpoint, generate Markdown docs, or feed it into a dashboard.\n\n---\n\n## Making Decisions Manually\n\nYou can call the engine directly (e.g. in services, background jobs, or tests):\n\n```ts\nimport type { Decision } from '@aegis-runtime/aegisauth';\nimport { engine } from './policies';\n\nconst ctx: Ctx = {\n  user: { id: 'u1', orgId: 'org1', roles: ['admin'] },\n};\n\nconst invoice: Invoice = {\n  id: 'inv1',\n  orgId: 'org1',\n  status: 'draft',\n};\n\nconst decision: Decision = engine.decide({\n  resourceType: 'invoice',\n  action: 'delete',\n  ctx,\n  resource: invoice,\n});\n\nif (decision.allowed) {\n  // proceed\n} else {\n  console.log('Denied because:', decision.reasons);\n}\n```\n\nBehavior:\n\n* If **any `cannot` rule** matches, the decision is denied.\n* Else if **any `can` rule** matches, the decision is allowed.\n* If no rule matches, the decision is an **implicit deny**.\n* If no rules exist for the given resource/action, AegisAuth returns a helpful reason message.\n\n---\n\n## Using the Express Adapter\n\nAegisAuth ships an Express middleware adapter that wires the engine into HTTP routes.\n\n### 1. Import the adapter\n\n```ts\nimport { authorize } from '@aegis-runtime/aegisauth/express';\nimport { engine } from '../auth/policies';\n```\n\n### 2. Protect a route\n\n```ts\nimport express from 'express';\n\nconst router = express.Router();\n\nrouter.delete(\n  '/:id',\n  authorize(engine, 'invoice', 'delete', async (req) => {\n    const user = req.user as User | null; // from your auth middleware\n    const invoice = await loadInvoiceFromDb(req.params.id);\n\n    return { ctx: { user }, resource: invoice };\n  }),\n  async (req, res) => {\n    const invoice = res.locals.resource as Invoice;\n    await deleteInvoice(invoice.id);\n    res.status(204).send();\n  }\n);\n```\n\n### 3. Middleware signature\n\n```ts\nfunction authorize<\n  Ctx,\n  Resources extends Record<string, any>,\n  K extends keyof Resources & string\n>(\n  engine: PolicyEngine<Ctx, Resources>,\n  resourceType: K,\n  action: string,\n  resolve: (req: Request) =>\n    | { ctx: Ctx; resource: Resources[K] }\n    | Promise<{ ctx: Ctx; resource: Resources[K] }>,\n  options?: AuthorizeOptions<Ctx, Resources, K>\n): RequestHandler;\n```\n\nAt runtime:\n\n1. `resolve(req)` is called to build `{ ctx, resource }`.\n2. `engine.decide({ resourceType, action, ctx, resource })` is executed.\n3. If **denied**:\n\n   * Default: responds with `403` and `{ error, reasons }` JSON\n   * Or, if `options.onDeny` is provided, your custom handler is called\n4. If **allowed**:\n\n   * Decision is attached to `res.locals[attachKey]` (default: `'authDecision'`)\n   * Resource is attached to `res.locals.resource` (optional)\n   * `next()` is called\n\n### 4. Customizing behavior\n\n```ts\nimport { authorize } from '@aegis-runtime/aegisauth/express';\n\nrouter.post(\n  '/',\n  authorize(\n    engine,\n    'invoice',\n    'create',\n    async (req) => ({\n      ctx: { user: req.user as User | null },\n      resource: req.body as Invoice,\n    }),\n    {\n      onDeny: (req, res, decision) => {\n        res.status(401).json({\n          error: 'Not allowed to create invoices',\n          reasons: decision.reasons,\n        });\n      },\n      attachDecisionTo: 'locals',   // or 'request'\n      attachKey: 'invoiceDecision', // res.locals.invoiceDecision\n      attachResource: true,\n    }\n  )\n);\n```\n\n---\n\n## Advanced Features\n\n### OpenTelemetry Integration\n\nAegisAuth provides built-in OpenTelemetry integration for production observability.\n\n#### Setup\n\n```ts\nimport { createPolicyEngineWithOtel } from '@aegis-runtime/aegisauth/otel';\nimport { Meter, Tracer } from '@opentelemetry/api';\n\n// In your OpenTelemetry setup\nconst meter = /* your OpenTelemetry Meter */;\nconst tracer = /* your OpenTelemetry Tracer */;\n\nconst engine = createPolicyEngineWithOtel({\n  meter,\n  tracer, // optional\n  defaultAttributes: {\n    'service.name': 'invoice-service',\n    'service.version': '1.0.0',\n  },\n});\n\n// Use the engine normally - metrics and traces are emitted automatically\nengine.forResource('invoice')\n  .can('read')\n  .because('User can read invoices');\n```\n\n#### Metrics Emitted\n\n- **`aegisauth_decisions_total`** (Counter): Total number of authorization decisions\n  - Attributes: `aegisauth.resource_type`, `aegisauth.action`, `aegisauth.allowed`\n- **`aegisauth_decision_duration_ms`** (Histogram): Latency of authorization decisions\n  - Unit: milliseconds\n  - Attributes: Same as counter\n\n#### Traces Emitted (if tracer provided)\n\n- **Span name**: `aegisauth.decision`\n- **Attributes**:\n  - `aegisauth.resource_type`\n  - `aegisauth.action`\n  - `aegisauth.allowed`\n  - `aegisauth.reasons` (joined with ` | `)\n  - `aegisauth.matched_rule_ids` (joined with `,`)\n  - Plus any `defaultAttributes` you provided\n\nThis enables monitoring authorization decisions in production, alerting on policy violations, and debugging authorization issues.\n\n### Shadow Testing\n\nShadow testing allows you to evaluate a candidate policy engine alongside your production engine without affecting user requests. This is invaluable for:\n\n- Testing new policies before deployment\n- Validating policy migrations\n- A/B testing authorization rules\n- Detecting policy regressions\n\n#### Setup\n\n```ts\nimport { createShadowEngine } from '@aegis-runtime/aegisauth/shadow';\nimport { authorizeWithShadow } from '@aegis-runtime/aegisauth/express';\n\n// Your current production engine\nconst currentEngine = createPolicyEngine<Ctx, Resources>();\n// ... define current policies\n\n// Your candidate engine with new/changed policies\nconst candidateEngine = createPolicyEngine<Ctx, Resources>();\n// ... define candidate policies\n\nconst shadowEngine = createShadowEngine({\n  current: currentEngine,\n  candidate: candidateEngine,\n  onDivergence: (info) => {\n    // Log or alert when decisions diverge\n    console.warn('Policy divergence detected:', {\n      resourceType: info.resourceType,\n      action: info.action,\n      current: info.current.allowed,\n      candidate: info.candidate.allowed,\n      currentReasons: info.current.reasons,\n      candidateReasons: info.candidate.reasons,\n    });\n    \n    // Send to monitoring/alerting system\n    // metrics.recordDivergence(info);\n  },\n});\n\n// Use shadow engine in routes - current engine's decision is enforced,\n// but candidate is evaluated in parallel\nrouter.delete(\n  '/:id',\n  authorizeWithShadow(shadowEngine, 'invoice', 'delete', async (req) => {\n    const user = req.user as User | null;\n    const invoice = await loadInvoiceFromDb(req.params.id);\n    return { ctx: { user }, resource: invoice };\n  }),\n  async (req, res) => {\n    // ... handler\n  }\n);\n```\n\n#### How It Works\n\n1. **Current engine's decision is enforced**: Users experience the behavior of your current policies\n2. **Candidate engine is evaluated in parallel**: The candidate engine's decision is computed but not used\n3. **Divergences are reported**: If decisions differ, `onDivergence` is called with both decisions\n4. **Zero user impact**: Even if the candidate engine would deny access, the user's request proceeds based on the current engine\n\n#### Divergence Detection\n\nA divergence is detected when:\n\n- `currentDecision.allowed !== candidateDecision.allowed`, OR\n- The matched rule IDs differ, OR\n- The reasons differ\n\nThis allows you to catch subtle policy changes that might affect authorization behavior.\n\n#### Decision Hooks\n\nYou can also use the `onDecision` hook on individual engines to capture timing and metrics:\n\n```ts\nconst engine = createPolicyEngine({\n  onDecision: (info) => {\n    console.log(`Decision took ${info.elapsedMs}ms`);\n    // Custom metrics, logging, etc.\n  },\n});\n```\n\n---\n\n## CLI: Route Authorization Report\n\nThe **CLI** scans your TypeScript source files for Express routes and reports which ones use `authorize(...)`.\n\n### 1. Human-readable report\n\nFrom your project root (where your routes live):\n\n```bash\nnpx @aegis-runtime/aegisauth report src\n```\n\nExample output:\n\n```text\nScanning routes under: /path/to/project/src\n\nAegisAuth route authorization report\n------------------------------------\n\n[ OK ]  GET    /invoices/:id        src/routes/invoices.ts:10\n[ OK ]  DELETE /invoices/:id        src/routes/invoices.ts:30\n[ !! ]  POST   /invoices            src/routes/invoices.ts:45\n\nSummary:\n  Total routes:          3\n  Protected (authorize): 2\n  Missing authorize:     1\n```\n\n* `[ OK ]` – route has at least one handler using `authorize(...)`\n* `[ !! ]` – route has no `authorize(...)` handler detected\n\nIf any route is missing authorization, the CLI returns **exit code 1**.\n\nThis is ideal for CI:\n\n```yaml\n# GitHub Actions example\n- name: AegisAuth route report\n  run: npx @aegis-runtime/aegisauth report src\n```\n\n### 2. JSON mode for tooling / dashboards\n\nYou can also get machine-readable JSON:\n\n```bash\nnpx @aegis-runtime/aegisauth report src --json > aegisauth-report.json\n```\n\nExample JSON shape:\n\n```json\n{\n  \"root\": \"/absolute/path/to/src\",\n  \"routes\": [\n    {\n      \"method\": \"GET\",\n      \"path\": \"/invoices/:id\",\n      \"file\": \"/absolute/path/to/src/routes/invoices.ts\",\n      \"line\": 10,\n      \"authorized\": true\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/invoices\",\n      \"file\": \"/absolute/path/to/src/routes/invoices.ts\",\n      \"line\": 45,\n      \"authorized\": false\n    }\n  ],\n  \"summary\": {\n    \"total\": 3,\n    \"protected\": 2,\n    \"missing\": 1\n  }\n}\n```\n\n### 3. Diff Reports\n\nCompare two reports to detect regressions:\n\n```bash\nnpx @aegis-runtime/aegisauth report src --json > report.old.json\n# ... make changes ...\nnpx @aegis-runtime/aegisauth report src --json > report.new.json\nnpx @aegis-runtime/aegisauth diff report.old.json report.new.json\n```\n\nOutput shows routes that gained or lost authorization protection.\n\nYou can:\n\n* Upload this as a CI artifact\n* Feed it into a dashboard\n* Diff it between branches to see how coverage changes\n\n### 4. What the CLI recognizes\n\nThe CLI currently supports **Express-style** route definitions:\n\n```ts\napp.get('/path', handler);\nrouter.post('/path', handler1, handler2);\n```\n\nIt looks for imports like:\n\n```ts\nimport { authorize } from '@aegis-runtime/aegisauth/express';\nimport { authorize as aegisAuthorize } from '@aegis-runtime/aegisauth/express';\n```\n\nIf any handler argument in the route call uses one of these `authorize` identifiers,\nthat route is considered **protected**.\n\nThe CLI does not execute your code; it performs static analysis using the TypeScript compiler API.\n\n---\n\n## Examples & Project Structure\n\nA typical usage structure might look like:\n\n```text\nsrc/\n  auth/\n    policies.ts        # all AegisAuth policies live here\n  routes/\n    invoices.ts        # Express routes using authorize(engine, ...)\n  db/\n    invoices.ts        # DB accessors\n  server.ts            # app bootstrap\n```\n\n* `auth/policies.ts`\n\n  * Defines `Ctx`, `Resources`, and all rules\n  * Is the single source of truth for authorization\n\n* `routes/*.ts`\n\n  * Import `authorize` from `@aegis-runtime/aegisauth/express`\n  * Wire policies into HTTP handlers via middleware\n\n* CI config\n\n  * Runs `npx @aegis-runtime/aegisauth report src` (optionally with `--json`) to ensure all routes are protected\n\n---\n\n## Design Notes\n\n### 1. Deny-overrides semantics\n\nAegisAuth adopts a simple yet robust model:\n\n* If **any deny rule** (`cannot`) matches, the final decision is **deny**.\n* Otherwise, if **any allow rule** (`can`) matches, the final decision is **allow**.\n* If no rule matches, the result is an **implicit deny** with a clear reason.\n\nThis mirrors common practices in secure systems (deny is sticky and safer).\n\n### 2. Type-safe DSL\n\nThe engine is generic over `Ctx` and `Resources`, so:\n\n* You get full TypeScript type-checking inside `when((ctx, resource) => ...)`\n* Mistyped fields on your `resource` or `ctx` are caught at compile time\n* You define policies in terms of your actual domain types\n\n### 3. No runtime dependencies in the core\n\nThe core engine:\n\n* Has **no external runtime dependencies**\n* Does **not** require a database, cache, or message queue\n* Is pure, deterministic logic → easy to test and reason about\n\nThe CLI is a separate concern, built on top of the TypeScript compiler API.\n\n### 4. Express-first, framework-agnostic\n\nThe included adapter targets **Express** because it's widely used, but the engine itself is framework-agnostic:\n\n* You can use `engine.decide(...)` in Nest, Fastify, RPC handlers, cron jobs, etc.\n* Thin adapters for other frameworks can be built easily.\n\n### 5. Performance Characteristics\n\n* **Rule lookup**: O(1) by resource type and action (Map-based storage)\n* **Decision evaluation**: O(n) where n is the number of rules for the resource/action pair\n* **Memory**: O(r) where r is the total number of rules\n* **No I/O**: All decisions are synchronous and in-memory\n\nTypical authorization decisions complete in microseconds, making AegisAuth suitable for high-throughput applications.\n\n---\n\n## Roadmap\n\nPlanned and potential enhancements:\n\n* **Policy testing helpers**\n  Utilities for writing focused unit tests and fixtures for complex policies.\n\n* **More framework adapters**\n  Fastify, NestJS decorators, RPC middleware, etc.\n\n* **Richer analysis & reporting**\n\n  * Mapping routes to specific `(resource, action)` pairs\n  * HTML/Markdown report generation\n\n* **Policy Explorer UI**\n  A small React app that ingests `@aegis-runtime/aegisauth report --json` + `engine.listRules()` and renders a role/action matrix.\n\n* **IDE integration**\n  VS Code extension to visualize which policies apply to a given route or role.\n\n* **Policy versioning**\n  Built-in support for policy versioning and migration strategies.\n\n* **Rule composition**\n  Higher-level abstractions for common patterns (RBAC, ABAC, etc.).\n\nIf you have use cases or ideas, feel free to open an issue or PR.\n\n---\n\n## License\n\nMIT. Use it freely in commercial and open-source projects.\n","readmeFilename":"README.md","_rev":"1-a1641c80fad3e96467161b8995f808e0"}