Kerberos v4: Implement critical fixes, hardening, and performance optimizations - #6
Closed
Alex-Dolid wants to merge 6 commits into
Closed
Kerberos v4: Implement critical fixes, hardening, and performance optimizations#6Alex-Dolid wants to merge 6 commits into
Alex-Dolid wants to merge 6 commits into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request updates documentation, configuration, and metadata for the
@alexify/kerberosproject, 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
onErroroption is clarified as the sole control for evaluation error behavior, and audit entries now includeprincipalRoles, fail-closed denials, and info-level plan results. [1] [2] [3]auditoption. [1] [2]Platform and Packaging
src/Kerberos.jsandsrc/Relations/RelationResolver.jsnow require./runtime/node.js, and documentation details how browser bundlers swap these for platform neutrality.Cache, Concurrency, and Policy Options
cacheRetryoption now supports backoff, timeouts, and degraded mode. New options includecacheKeyPrefix,relationsTimeoutMs,auditfor audit enrichment, andmaxConcurrencyfor resource evaluation. [1] [2]Changelog, Licensing, and Linting
no-undef,no-dupe-else-if, and others.These updates collectively improve the robustness, clarity, and maintainability of the project, especially around observability, platform support, and operational safety.