Skip to content

Kerberos v4: Implement critical fixes, hardening, and performance optimizations - #6

Closed
Alex-Dolid wants to merge 6 commits into
mainfrom
review/wave-0-critical-fixes
Closed

Kerberos v4: Implement critical fixes, hardening, and performance optimizations#6
Alex-Dolid wants to merge 6 commits into
mainfrom
review/wave-0-critical-fixes

Conversation

@Alex-Dolid

Copy link
Copy Markdown
Contributor

This pull request updates documentation, configuration, and metadata for the @alexify/kerberos project, focusing on improved observability, platform neutrality, and more robust error handling. The changes clarify the platform split, enhance logging and audit completeness, expand cache and concurrency options, and update size and dependency information to reflect recent codebase growth.

Key documentation and configuration updates:

Observability, Logging, and Error Handling

  • Enhanced logging and audit documentation: Logging is now described as pure observability, always guarded so logger errors never affect authorization decisions. The onError option is clarified as the sole control for evaluation error behavior, and audit entries now include principalRoles, fail-closed denials, and info-level plan results. [1] [2] [3]
  • Audit completeness and fail-closed denials are now explicitly covered, including how errors are surfaced in logs and metrics, and how audit tracing can be enforced with the audit option. [1] [2]

Platform and Packaging

  • Clarified platform runtime split: Both src/Kerberos.js and src/Relations/RelationResolver.js now require ./runtime/node.js, and documentation details how browser bundlers swap these for platform neutrality.
  • Updated project/package size: Documentation and badges now reflect the increased bundle size (~29 KB min+gzip) due to new features and improved observability. [1] [2] [3]

Cache, Concurrency, and Policy Options

  • Expanded cache and concurrency options: The cacheRetry option now supports backoff, timeouts, and degraded mode. New options include cacheKeyPrefix, relationsTimeoutMs, audit for audit enrichment, and maxConcurrency for resource evaluation. [1] [2]
  • Documented per-batch and cross-request memoization, and clarified concurrency caps for both policy evaluation and relation lookups.

Changelog, Licensing, and Linting

  • Updated changelog for unreleased features, including OpenTelemetry support, browser/server entrypoint split, and new error handling and audit features. [1] [2] [3] [4]
  • Updated license years to 2024–2026.
  • Added stricter linting rules for code quality, including no-undef, no-dupe-else-if, and others.
  • Minor improvements to developer documentation and scripts, including test, lint, format, and size commands.

These updates collectively improve the robustness, clarity, and maintainability of the project, especially around observability, platform support, and operational safety.

Alex-Dolid and others added 6 commits August 23, 2026 18:09
Security / correctness:
- Conditions: inherited-key strategy lookups (constructor/toString/valueOf)
  no longer turn a broken conditional rule into an unconditional match —
  own-property guard, fail-closed (+regression tests)
- Scope depth capped at 16 dot-segments (KerberosValidationError, propagates
  even under onError:'deny') and scope strings at 512 chars across all three
  validation backends — closes a cache-read amplification / quadratic-CPU
  request vector

ESM interop:
- src/index.js is now built only from ...require('./file.js') re-export
  spreads (new src/publicExports.js carries the codec names + PlanKind):
  cjs-module-lexer bailed on the local-variable spread and silently dropped
  19 names (codec fns, PlanKind, Effect, schemas, validation helpers) from
  the ESM named surface. Full 64/64 CJS<->ESM parity restored; EXPR_META /
  evalExprAst stay internal (+ESM smoke tests)

Performance:
- Engine log helpers early-return when the logger is disabled (mirrors
  RelationResolver#logDebug) — no timestamp/entry allocation per call:
  simple isAllowed 317k -> 513k ops/sec (+63%), cache-backed 141k -> 249k
  (+77%) on the bench harness

Resilience / observability:
- RelationResolver session memos evict rejected singleflight promises, so a
  transient backend failure never poisons a shared memo (+test, README and
  docs lifetime note)
- winston/consola-shaped loggers (which expose .log alongside info/debug)
  now route to the structured writer instead of silently losing the audit
  trail (+regression test)
- Metadata response schemas accept the engine's real includeMeta output:
  optional matchedPolicy, deny-reason enum, resolution trace union in all
  three backends (+round-trip test against live checkResources meta)

Docs:
- CLAUDE.md: replace the stale pre-v3 logging<->error coupling claim with
  the actual onError contract, correct the runtime-split requirer list, the
  pnpm version reference and the lint scope
- LICENSE year range 2024-2026

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cache reader (createCacheReader):
- Full-jitter exponential backoff between retry attempts (delayMs base 25ms,
  doubling per attempt, jitter on by default; delayMs: 0 restores immediate
  retries) — back-to-back retries defeated the mechanism and amplified load
  on struggling backends
- Deterministic adapter errors (TypeError/SyntaxError) are no longer retried
- Optional timeoutMs bounds each read attempt via a shared withTimeout helper
  (new src/async.js, platform-neutral) — a hung backend fails as
  KerberosCacheError instead of hanging authorization; the no-timeout hot
  path stays a direct awaited call

Engine options:
- cacheRetry.onExhausted: 'miss' — opt-in degraded mode: exhausted retries
  count as a cache miss so evaluation falls through to static sources; a
  cache outage no longer disables statically-resolvable decisions (visible
  via the cache 'error' metric + guarded log entry)
- cacheKeyPrefix — namespaces ALL cache keys (policies + derived roles) for
  multi-tenant/shared-store deployments; closes the silent cross-tenant
  derivedRoles:<name> collision
- relationsTimeoutMs — bounds relations.check/list calls; a hung resolver
  fails as KerberosRelationsError per onError

Batch resolution memo:
- checkResources shares one singleflight lookups memo across the batch (and
  planResources across its role closure): each distinct policy/derived-roles
  document resolves once per batch instead of once per resource — a
  20-resource batch went from 40 duplicate cache reads to 2 — with trace
  entries replayed into every resource's meta.resolution. Gated on
  cache.enabled; single-shot isAllowed and static-only configs keep an
  allocation-free fast path (bench: simple 525k ops/s, static batch 41.2k,
  cache-backed 260k — all at or above the pre-change numbers)

checkResources error contract:
- Error-shaped batch denials are now marked { reason: 'evaluation-error',
  errorName } under includeMeta, so an outage is distinguishable from a
  policy DENY; documented that onError applies at request level only
  (README + docs wording fixed accordingly)

Reverse lookups (RelationResolver):
- onTruncated: 'throw' option — a maxResults truncation of lookupSubjects /
  lookupResources raises a typed KerberosRelationsError instead of silently
  narrowing the result; truncation always recorded on the span as
  kerberos.result.truncated
- expandRelationOperands accepts an { ids, truncated } envelope and degrades
  a truncated relation branch to the sound `opaque` post-filter operator
  instead of baking an incomplete id list into the plan

Types & docs: KerberosCacheRetry type, new options in index.d.ts /
relations.d.ts, 'evaluation-error' reason (+ errorName) in the d.ts and all
three Metadata schema backends, README/docs option tables and truncation
contract; 14 new regression tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit stream completeness:
- Fail-closed denials now reach the audit log and the kerberos.decisions
  counter: a resource whose evaluation rejects inside a checkResources batch
  logs its DENY decisions marked { reason: 'evaluation-error', errorName },
  and the onError:'deny' fallback of isAllowed emits an equivalent decision
  entry — previously the most security-relevant denials (cache outages) were
  exactly the ones missing from the decision stream
- Audit entries record principalRoles (the role set the decision was based
  on); roles change over time, so past entries stay explainable
- New engine option audit: { includeMeta: true } — decision tracing runs for
  every request when a logger is attached, so audit entries carry
  meta.resolution and the policy-miss reason regardless of the caller's
  per-request includeMeta response flag (the response stays gated)

Decision trace:
- Derived-roles imports are now traced: { source: 'derivedRoles', name,
  matched, origin? } entries in meta.resolution — previously the one
  cache-backed resolution step invisible to the trace (an evicted/corrupt
  document silently stopped rules from matching with no hint); Metadata
  schemas (all three backends) and index.d.ts extended accordingly

Log levels:
- PlanResources.result audit entries go out at INFO level (new writer.info
  channel; structured -> sink.info, legacy -> logger.info/log): an
  ALWAYS_ALLOWED (fail-open) plan now survives the docs' own
  pino({ level: 'info' }) production recipe instead of vanishing with the
  lifecycle debug events

Swallowed-failure visibility:
- New kerberos.observability.failures counter (sink: logger|telemetry):
  logger/telemetry sink failures stay swallowed (the never-affect-
  authorization contract) but are no longer invisible; the engine also
  console.warns once per instance on the first swallowed logger failure

Relations seam:
- The request span now carries kerberos.relations.count and
  kerberos.relations.duration_ms measured at the engine seam, so relation
  latency is attributable even with a custom resolver that has no
  instrumentation of its own (built-in resolver metrics stay inside the
  resolver to avoid double counting)
- Fixed the stale kerberos.request.duration description (it also records
  PlanResources and Relations* calls)

Docs: telemetry tables (six instruments, planResources span attributes),
decision-metadata resolution entry shapes, configuration/README option
lists and audit-entry field docs; 10 new regression tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Synchronous evaluation driver (the big one):
- Fully-synchronous configurations (no cache, no relations — the
  zero-dependency baseline) now evaluate through a sync driver that skips
  interior promise allocation and microtask hops entirely (~10 awaited
  frames per request before). Layering/merge semantics are NOT duplicated:
  both drivers share #applyParentRoleResult / #mergeRoleResultInto /
  #mergeSourceResults, and the new test/SyncAsyncParity.test.js pins
  end-to-end driver equivalence (responses byte-identical incl. meta)
- Bench: simple isAllowed 514k -> ~800k ops/s (+55%), derived-roles 405k ->
  ~650k, static 10-resource batch 39k -> ~63k

Validate once, not three times:
- isAllowed/checkResources/planResources validate ARGUMENTS once with the
  constructor-precompiled validator and assemble the internal request
  without the old buildRequest re-parse — under Zod the same
  principal/resource was deep-parsed up to 3x per call (1+2N per batch) and
  P/principal ended up as different clones (identity now restored). The
  public static Kerberos.parseRequest keeps full validation for external
  callers; new Zod-backend bench scenario records the cost (~470k ops/s)

Cache-path work reduction:
- Cross-request instance memo in #resolveFromCache keyed by the IDENTITY of
  the raw cached value (WeakMap; bounded per-key map for string values):
  an unchanged document skips deserialize+validate+construct entirely while
  TTL/invalidation stays backend-owned (new reference = rebuild). Bench:
  cache-backed isAllowed 243k -> ~330k ops/s (+36%); tests cover reuse,
  replacement-invalidation and string-equality memoization
- Per-role policy lookups resolve as one settled wave on the cache path
  (no short-circuit exists to lose), with per-role trace buffers keeping
  meta.resolution deterministic; the planner's parentRoles closure BFS is
  level-batched (RelationResolver #subjectClosure pattern) — O(depth)
  round-trip waves instead of O(roles) sequential ones

Bounded fan-out:
- New maxConcurrency option on the engine (checkResources batch chains) and
  the RelationResolver (lookupResources candidate verification) — a
  zero-dependency FIFO limiter in src/async.js; settleAll now lives there
  too, shared by engine and resolver (one home for the documented
  parallelism policy)

Minor:
- Policy/derived-roles check() skips the two per-check request spreads when
  the policy declares neither constants nor variables (the common case)
- Bench harness covers the previously-blind paths: role+parentRoles chain,
  scoped requests, Zod validation, includeMeta, cache-backed 50-resource
  batch; README benchmark table refreshed with the new numbers

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drift guards (the checks the hand-maintained d.ts relied on humans for):
- Six correctness lint rules opted back in per-rule (no-undef above all —
  the only static guard against typo'd identifiers in a codebase of
  guarded catch blocks); the category itself stays off as documented
- test/tests.test-d.ts pulls the /tests subpath declarations into tsd —
  which immediately surfaced two latent errors that had shipped unchecked:
  tests.d.ts imported from the package name (never resolvable) and shadowed
  three classes with parameters named after them (self-referential types)
- test/ExportParity.test.js diffs runtime Object.keys of all three entry
  points against each d.ts's declared value exports, both directions
- The six public static parse* helpers (and the Tests DSL statics) are now
  declared in index.d.ts / tests.d.ts — closing the drift the parity guard
  would have caught

Hardening:
- Policy shapes are deep-frozen after construction and the constructors now
  CLONE the plain spine of their input first (cloneShapeTree in the new
  src/freeze.js, shared with the codec's AST freezing): live engine state
  can no longer be rewritten via policy.shape, the caller's literal is
  neither mutated (pre-existing wart) nor frozen, and the same literal can
  construct many instances; Effect is frozen like PlanKind
- codec: evalNew gets the own-property constructor guard (defense in depth,
  mirrors evalCall); new maxBuiltStringLength limit (default 1M chars) caps
  strings BUILT by repeat/padStart/padEnd — a tiny expression could
  otherwise allocate ~0.5GB per evaluation, exactly the compromised-store
  memory-exhaustion the codec's threat model promises to prevent
- RelationResolver #validKinds is seeded from the schema's type names and
  no longer caches caller-supplied kinds — a fuzzing caller could grow the
  set without bound

Behavior improvements:
- checkResources onError:'deny' fallback returns one all-DENY result per
  requested resource (positional parity like Cerbos) instead of results: []
  which crashed positional consumers during the incident 'deny' exists to
  survive (+end-to-end test via the one real request-level failure path)
- Resolver spans accept opts.callId (kerberos.call_id attribute); the
  engine passes its kerberosCallId through the relations seam automatically
- Scope-chain memo evicts LRU at capacity instead of freezing on the first
  1000 scopes; codec AST cache gets an LRU touch and drops the has/get pair
- Reverse static tuple index builds lazily on first reverse-API use —
  check/list-only deployments no longer pay double index memory
- Subject-set algebra extracted to src/Relations/subjectSet.js with a
  table-driven unit matrix (concrete × wildcard × exclusion across
  union/intersection/subtraction); settleAll shared via src/async.js
- Constructor validator wiring table-driven: one backend dispatch + one
  builder-name list instead of three hand-synced 7-assignment blocks

Docs & meta:
- CHANGELOG: 3.x link refs, mis-filed 3.0.0 additions moved to Added,
  duplicate Changed merged, [Unreleased] section
- README/docs: static-over-cached scope-shadowing warning (hybrid
  deployments), deny-fallback shape, codec limits, callId option;
  CLAUDE.md documents pnpm size

Deliberately skipped: #runtime imports-field alias (would break webpack 4 —
the legacy browser map cannot remap a '#'-specifier, defeating the
finding's own premise)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pnpm size moved from 25.1/15.1 KB to 28.6/16.3 KB (min+gzip) for the
main entry and /relations subpath as the engine grew across the
review waves (memoization, resilience options, frozen shapes, etc.).
Synced every README/docs/CLAUDE.md mention, including CLAUDE.md's
long-stale "~8 KB" figure left over from before ReBAC/planning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
kerberos Ready Ready Preview Aug 24, 2026 5:23pm

@Alex-Dolid Alex-Dolid changed the title Kerberos v3.2: Implement critical fixes, hardening, and performance optimizations Kerberos v4: Implement critical fixes, hardening, and performance optimizations Aug 24, 2026
@Alex-Dolid Alex-Dolid closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant