Skip to content

Ofrep local cache#1485

Draft
jonathannorris wants to merge 2 commits intomainfrom
cursor/ofrep-local-cache-224a
Draft

Ofrep local cache#1485
jonathannorris wants to merge 2 commits intomainfrom
cursor/ofrep-local-cache-224a

Conversation

@jonathannorris
Copy link
Copy Markdown
Member

Add local cache support to the OFREP web provider to improve resilience and startup performance.

The local cache allows the provider to hydrate from a persisted snapshot on initialization, reducing initial load times. It also enables graceful fallback to the last known good configuration during transient network errors or temporary 5xx responses, preventing service disruption. Cache identity is tied to authorization and targeting keys to prevent cross-identity data leakage.


Open in Web Open in Cursor 

jonathannorris and others added 2 commits March 7, 2026 14:26
@cursor
Copy link
Copy Markdown

cursor bot commented Mar 7, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 7, 2026

Hey there and thank you for opening this pull request! 👋🏼

We require pull request titles to follow the Conventional Commits specification and it looks like your proposed title needs to be adjusted.
Details:

No release type found in pull request title "Ofrep local cache". Add a prefix to indicate what kind of release this pull request corresponds to. For reference, see https://www.conventionalcommits.org/

Available types:
 - feat: A new feature
 - fix: A bug fix
 - docs: Documentation only changes
 - style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
 - refactor: A code change that neither fixes a bug nor adds a feature
 - perf: A code change that improves performance
 - test: Adding missing tests or correcting existing tests
 - build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
 - ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
 - chore: Other changes that don't modify src or test files
 - revert: Reverts a previous commit

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the OFREP web provider by integrating a robust local caching system. This system improves the provider's resilience against network failures and temporary server issues by allowing it to operate from a previously stored configuration. It also boosts startup performance by hydrating from a local snapshot, ensuring a smoother user experience. The cache is designed with security in mind, preventing data leakage by associating cached data with specific authorization and targeting contexts.

Highlights

  • Local Cache Implementation: Introduced a local cache mechanism for the OFREP web provider, leveraging localStorage by default, to persist bulk evaluation responses.
  • Improved Resilience: Enabled graceful fallback to the last known good configuration from the local cache during transient network errors, timeouts, or temporary 5xx HTTP responses, preventing service disruption.
  • Enhanced Startup Performance: Allowed the provider to hydrate from a persisted snapshot on initialization, significantly reducing initial load times by avoiding immediate network requests.
  • Secure Cache Identity: Implemented a cache identity mechanism tied to the Authorization header hash and targetingKey to prevent cross-identity data leakage and ensure data integrity.
  • Configurable Cache Behavior: Added new options (disableLocalCache, storageKey, storageAdapter) to OFREPWebProviderOptions for customizing or disabling the local cache functionality.
  • New Error Type for Transient Issues: Introduced OFREPApiTransientHttpError to specifically handle 5xx HTTP responses, allowing for more granular error management and cache fallback logic.
Changelog
  • libs/providers/flagd-web/project.json
    • Added buildValidators target to the project configuration.
  • libs/providers/flagd/project.json
    • Added buildValidators target to the project configuration.
  • libs/providers/ofrep-web/README.md
    • Added comprehensive documentation for the new local cache feature, including usage, customization, and disabling instructions.
  • libs/providers/ofrep-web/src/index.ts
    • Exported the new ofrep-web-provider-options model.
  • libs/providers/ofrep-web/src/lib/cacheIdentity.ts
    • Added a new utility file to resolve a unique cache key hash based on authorization and targeting context, including a SHA-256 hashing mechanism with a fallback.
  • libs/providers/ofrep-web/src/lib/model/ofrep-web-provider-options.ts
    • Defined the OFREPWebProviderStorage interface for custom storage adapters.
    • Extended OFREPWebProviderOptions with disableLocalCache, storageKey, and storageAdapter properties.
  • libs/providers/ofrep-web/src/lib/model/persistedCacheSnapshot.ts
    • Added a new file defining the structure and utility functions for PersistedCacheSnapshot, including serialization, deserialization, and type guards.
  • libs/providers/ofrep-web/src/lib/ofrep-web-provider.spec.ts
    • Updated test setup to mock localStorage using a custom storage adapter.
    • Added new test cases to verify local cache bootstrapping on network failures and 5xx responses.
    • Included tests to ensure cache is not used on 4xx errors or cache key mismatches.
    • Verified ETag reuse for If-None-Match requests.
    • Added tests for cache replacement after successful 200 responses and disabling local cache functionality.
  • libs/providers/ofrep-web/src/lib/ofrep-web-provider.ts
    • Imported new error types and cache-related modules.
    • Added internal state variables for cache management, including currentCacheKeyHash and hasCurrentState.
    • Modified _fetchFlags to incorporate cache loading, state application, and persistence logic.
    • Implemented isTransientError to identify errors suitable for cache fallback.
    • Added private methods applyCurrentState, resetCurrentState, loadPersistedSnapshot, persistCurrentState, and getStorageAdapter to manage cache state and interaction with storage.
  • libs/shared/flagd-core/src/lib/generated/validators.d.ts
    • Added a new file containing type declarations for pre-compiled validators.
  • libs/shared/ofrep-core/src/lib/api/errors.ts
    • Added a new error class, OFREPApiTransientHttpError, to represent transient HTTP errors (e.g., 5xx status codes).
  • libs/shared/ofrep-core/src/lib/api/ofrep-api.spec.ts
    • Added test cases to verify that OFREPApiTransientHttpError is thrown for 500 responses during flag evaluation.
  • libs/shared/ofrep-core/src/lib/api/ofrep-api.ts
    • Imported OFREPApiTransientHttpError.
    • Modified error handling in _fetch to throw OFREPApiTransientHttpError for 5xx HTTP status codes.
  • libs/shared/ofrep-core/src/test/handlers.ts
    • Added mock handlers to simulate 500 HTTP responses for testing purposes.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces local cache support for the OFREP web provider, enhancing resilience and performance. However, a critical security vulnerability has been identified: the cache identity hash does not include the server's base URL, which can lead to data leakage between different provider instances sharing the same credentials. This is compounded by the use of a weak 32-bit hash fallback and a fixed default storage key, increasing the risk of collisions and cross-user interference. From a code quality perspective, a critical issue was also found in the cache snapshot validation logic, which could allow invalid data to be loaded from the cache. Additionally, improvements are needed for logging silent failures during cache operations to enhance debuggability.

Comment on lines +81 to +107
function isEvaluationResponse(value: unknown): value is EvaluationResponse {
if (!isRecord(value)) {
return false;
}

if (isEvaluationFailureResponse(value)) {
return value.errorDetails === undefined || typeof value.errorDetails === 'string';
}

if ('key' in value && value['key'] !== undefined && typeof value['key'] !== 'string') {
return false;
}

if ('reason' in value && value['reason'] !== undefined && typeof value['reason'] !== 'string') {
return false;
}

if ('variant' in value && value['variant'] !== undefined && typeof value['variant'] !== 'string') {
return false;
}

if ('metadata' in value && value['metadata'] !== undefined && !isMetadataCache(value['metadata'])) {
return false;
}

return true;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The isEvaluationResponse type guard does not correctly validate a successful evaluation response. It's missing checks for the required key and value properties. According to the EvaluationSuccessResponse type, both are mandatory. This could lead to loading malformed data from the cache and cause runtime errors.

function isEvaluationResponse(value: unknown): value is EvaluationResponse {
  if (!isRecord(value)) {
    return false;
  }

  if (isEvaluationFailureResponse(value)) {
    return value.errorDetails === undefined || typeof value.errorDetails === 'string';
  }

  if (typeof value['key'] !== 'string') {
    return false;
  }

  if (!('value' in value)) {
    return false;
  }

  if ('reason' in value && value['reason'] !== undefined && typeof value['reason'] !== 'string') {
    return false;
  }

  if ('variant' in value && value['variant'] !== undefined && typeof value['variant'] !== 'string') {
    return false;
  }

  if ('metadata' in value && value['metadata'] !== undefined && !isMetadataCache(value['metadata'])) {
    return false;
  }

  return true;
}

Comment on lines +7 to +15
export async function resolveCacheKeyHash(
options: OFREPWebProviderOptions,
context?: EvaluationContext,
): Promise<string> {
const headers = await buildHeaders(options);
const authorizationValue = headers.get('authorization') ?? '';
const targetingKey = typeof context?.targetingKey === 'string' ? context.targetingKey : '';
return hashValue(`${authorizationValue}\u0000${targetingKey}`);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The resolveCacheKeyHash function does not include the baseUrl from the provider options in the generated hash. This creates a significant security risk where different provider instances (e.g., pointing to different OFREP servers) that share the same authorization credentials and targeting context will produce identical cache key hashes. Since the default storageKey is also fixed (ofrepLocalCache), these instances will overwrite each other's cache entries in localStorage. More critically, because the hashes match, one provider instance will successfully load and use cached flags that were actually fetched from a completely different server, leading to cross-provider data leakage and potential security misconfigurations.

Suggested change
export async function resolveCacheKeyHash(
options: OFREPWebProviderOptions,
context?: EvaluationContext,
): Promise<string> {
const headers = await buildHeaders(options);
const authorizationValue = headers.get('authorization') ?? '';
const targetingKey = typeof context?.targetingKey === 'string' ? context.targetingKey : '';
return hashValue(`${authorizationValue}\u0000${targetingKey}`);
}
export async function resolveCacheKeyHash(
options: OFREPWebProviderOptions,
context?: EvaluationContext,
): Promise<string> {
const headers = await buildHeaders(options);
const authorizationValue = headers.get('authorization') ?? '';
const targetingKey = typeof context?.targetingKey === 'string' ? context.targetingKey : '';
const baseUrl = options.baseUrl ?? '';
return hashValue(`${baseUrl}\u0000${authorizationValue}\u0000${targetingKey}`);
}

Comment on lines +27 to +36
function fallbackHash(value: string): string {
let hashValue = 0x811c9dc5;

for (const entry of textEncoder.encode(value)) {
hashValue ^= entry;
hashValue = Math.imul(hashValue, 0x01000193);
}

return (hashValue >>> 0).toString(16).padStart(8, '0');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The fallbackHash function uses the FNV-1a 32-bit hashing algorithm when SubtleCrypto is unavailable. While FNV-1a is fast, it is not a cryptographic hash and is highly susceptible to collisions due to its small 32-bit output space. Since this hash is used to verify the identity of a cached snapshot (including the Authorization header), a collision could allow one user to successfully load and use another user's cached flags if they share the same browser/origin. In a large user base, 32-bit collisions are statistically likely. It is recommended to use a more robust hashing algorithm or a JS-based cryptographic fallback (like SHA-256) to ensure secure cache isolation.

Comment on lines +293 to +295
} catch {
return undefined;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This catch block silently ignores any errors that occur during the loading of the persisted snapshot. For better observability and easier debugging of caching issues, it would be beneficial to log these errors.

    } catch (err) {
      this._logger?.warn('Unable to load persisted snapshot', err);
      return undefined;
    }

Comment on lines +315 to +317
} catch {
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This catch block silently ignores errors during the persistence of the cache state. Logging these errors would be helpful for diagnosing potential issues with the caching mechanism.

    } catch (err) {
      this._logger?.warn('Unable to persist current state', err);
      return;
    }

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.

4 participants