Skip to content

Fix cache security hardening#101

Merged
flyingsquirrel0419 merged 11 commits into
mainfrom
security-hardening-docs
Jul 16, 2026
Merged

Fix cache security hardening#101
flyingsquirrel0419 merged 11 commits into
mainfrom
security-hardening-docs

Conversation

@flyingsquirrel0419

@flyingsquirrel0419 flyingsquirrel0419 commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Description

Hardens cache security behavior across HTTP middleware, namespaces, protected disk payloads, generation cleanup, telemetry, CLI operations, Redis invalidation, snapshots, write coordination, structured cache keys, rate limiting, and the docs playground.

This update also closes all 10 actionable findings from the detailed follow-up review of PR #101. The fixes are covered by deterministic race/security regressions, real Redis integration tests, public migration guidance, and an architecture decision record.

Type of Change

  • Bug fix
  • New feature
  • Breaking behavior change
  • Refactor
  • Documentation update
  • Test coverage improvement

Follow-up Review Fixes

  1. Implicit Express/Hono caching now treats client_secret, client_assertion, and client_assertion_type as sensitive and bypasses URL-only caching unless a custom keyResolver is supplied.
  2. Generation cleanup keeps its streaming batches and now caps the cross-layer unique-key set at 10,000 matches by default.
  3. Per-key invalidation epochs use monotonic tokens and rotate the absent-key token during pruning, closing the ABA stale-write window.
  4. In-flight write-behind operations recheck their fence after the backend write and remove stale values from the affected layer.
  5. Retained write-ordering promises and key strings have finite global, active-key, and per-key admission limits, with explicit CacheWriteSaturationError backpressure.
  6. Plain objects can no longer forge native $type tags used for Date, URL, RegExp, Map, and Set cache keys; structured keys are versioned as j2:.
  7. A newly ready per-key fetch bucket can preempt a later shared drain timer, preserving cross-key rate-limit isolation.
  8. Single-key and mset() writes share one per-key ordering boundary, so stale cleanup cannot delete a newer bulk value.
  9. Every wildcard-only CLI invalidation pattern made from * and ? requires --force, including ** and ?*.
  10. The playground's token-bearing worker sender is lexical inside an IIFE and is no longer replaceable through Function()/worker globals.

Original PR Hardening

  • Keeps namespace clearing delimiter-bound.
  • Rejects legacy plaintext DiskLayer payloads by default when encryption or signing is configured.
  • Hashes OpenTelemetry cache-key attributes by default.
  • Scrubs Redis URLs from nested CLI errors and warns when Redis invalidation runs unsigned with a logger.
  • Revalidates snapshot parent paths before commit.
  • Runs playground examples in a sandboxed iframe and Blob Worker with token-checked messages.

Compatibility and Migration Notes

  • Automatically derived structured wrap() argument keys rotate from j: to j2:. Existing entries become cold misses and can expire naturally.
  • Plain domain objects using a reserved native $type tag must use a custom keyResolver.
  • generationCleanup: true defaults to maxMatches: 10_000. maxMatches: false is an explicit opt-out for deployments that impose another keyspace bound.
  • Write coordination defaults are 10,000 pending key-write units, 10,000 active keys, and 1,000 pending operations per key. Saturation is surfaced as backpressure rather than retaining unbounded work.
  • OAuth client credentials now bypass implicit HTTP caching, and wildcard-only CLI invalidation requires explicit --force intent.

The rationale, module responsibilities, data flow, alternatives, and tradeoffs are recorded in docs/adr/0001-security-hardening-boundaries.md. API, migration, resilience, CLI, integrations, changelog, README, and security documentation were updated with the same behavior.

Verification

  • Regression-first proof: the focused suite failed in 12 places across 8 files before the fixes.
  • Focused regression suite: 10 files, 193 tests passed.
  • npm run test:coverage: 45 files passed, 5 skipped; 665 tests passed, 24 skipped.
  • Coverage: 97.00% statements, 92.88% branches, 98.20% functions, 97.54% lines.
  • npm run test:integration: 6 files, 25 real Redis integration tests passed.
  • npm run lint.
  • npm run build with ESM, CJS, and declaration output.
  • npm --prefix docs-web run lint.
  • npm --prefix docs-web run build.
  • npm pack --dry-run plus ESM and CJS runtime smoke tests.
  • Default-limit probes: write 10,001 rejected at the 10,000 pending-write limit; generation cleanup stopped on match 10,001 after deleting 10,000 keys.
  • Original epoch-pruning PoC no longer reproduced: the old exploit expected epoch 0 after pruning and received monotonic token 50002.

Checklist

  • Existing style is preserved; no type errors are suppressed.
  • Security and concurrency changes include regression coverage.
  • Public behavior and migration notes are documented.
  • CHANGELOG.md is updated.
  • Build, coverage, integration, docs, packaging, and smoke checks pass.

Additional Notes

allowLegacyPlaintext, includeRawKeyAttributes, generationCleanup.maxMatches: false, and higher write-coordination limits are explicit opt-ins that should be reviewed against deployment-specific trust and memory boundaries.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements multiple security hardening measures: HTTP middleware (Express/Hono) now bypasses implicit caching for sensitive query parameters; DiskLayer/PayloadProtection rejects legacy plaintext entries by default when encryption/signing is configured; CacheNamespace.clear() uses a delimiter-scoped prefix; snapshot atomic writes check for symlinked parent directories; OpenTelemetry spans emit layercache.key_hash instead of raw keys by default; the CLI guards --pattern '*' behind --force and masks Redis URLs in error output; RedisInvalidationBus warns when constructed without signingSecret; generation cleanup switches to streaming; and the docs playground is extracted into a sandboxed iframe/worker architecture. Tests and documentation are updated throughout.

Changes

Security Hardening, Observability, and Playground Refactor

Layer / File(s) Summary
HTTP cache sensitive-query bypass (Express & Hono)
src/integrations/httpCacheKeys.ts, src/integrations/express.ts, src/integrations/hono.ts, tests/integrations/Integrations.test.ts
inspectHttpCacheUrl refactored to return both normalizedUrl and hasSensitiveQuery; both middlewares skip implicit caching when sensitive query parameters are detected without a custom keyResolver. Integration tests assert bypass and custom-resolver re-enablement.
DiskLayer & PayloadProtection legacy plaintext rejection
src/internal/PayloadProtection.ts, src/layers/DiskLayer.ts, tests/internal/PayloadProtection.test.ts, tests/layers/DiskLayer.test.ts
allowLegacyPlaintext added to PayloadProtectionOptions and DiskLayerOptions; unprotect throws PayloadProtectionError for plaintext payloads when protection is active and opt-in is absent. Tests cover rejection, file cleanup, and migration opt-in.
CacheNamespace.clear() delimiter-scoped prefix fix
src/CacheNamespace.ts, tests/CacheNamespace.test.ts
clear() now uses namespaceKeyPrefix() returning ${prefix}: to prevent invalidating sibling namespaces; regression test validates namespace isolation.
Snapshot atomic write symlinked parent rejection
src/internal/CacheSnapshotFile.ts, src/internal/CacheStackSnapshotManager.ts, tests/internal/CacheSnapshotFile.test.ts
commitAtomicWrite validates parent directory is not a symlink and, when options provided, that real parent stays within snapshotBaseDir; tests cover normal writes and race conditions with symlink injection.
OpenTelemetry key hashing by default
src/integrations/opentelemetry.ts, tests/features/GrowthFeatures.test.ts
createOpenTelemetryPlugin gains optional options.includeRawKeyAttributes; sanitizeAttributes replaces raw layercache.key with SHA-256 layercache.key_hash by default. Tests updated for hashed attributes and opt-in path.
CLI invalidation pattern guard and Redis URL masking
src/cli.ts, tests/cli.test.ts
requiresForceForInvalidationPattern centralizes bulk-pattern detection so --pattern '*' requires --force; maskRedisUrlsInText redacts Redis URLs from nested error messages. Tests cover both behaviors.
RedisInvalidationBus unsigned-mode warning
src/invalidation/RedisInvalidationBus.ts, tests/invalidation/RedisInvalidationBus.test.ts
Constructor emits logger?.warn when signingKey is absent; tests assert warning content, non-object payload rejection, and signature-helper guard.
Generation cleanup streaming via CacheKeyDiscovery
src/internal/CacheKeyDiscovery.ts, src/CacheStack.ts, tests/internal/CacheKeyDiscovery.test.ts, tests/internal/CacheStackInternals.test.ts
forEachKeyWithPrefix refactored around shared visit/assertWithinMatchCount; cleanupGeneration streams keys incrementally and flushes batches instead of bulk-collecting.
Playground sandbox runner extraction and hardening
docs-web/lib/playground/runner-source.ts, docs-web/lib/playground/sandboxed-runner.ts, docs-web/components/playground/PlaygroundClient.tsx, tests/docs/PlaygroundWorkerSandbox.test.ts, docs-web/content/playground.mdx, docs-web/content/index.mdx
Previous inline worker removed; new runner-source.ts generates the full cache simulation worker with MockCacheStack, MockCacheLayer, deduplication, SWR, and circuit breaker; sandboxed-runner.ts wraps execution in a CSP-restricted allow-scripts iframe with per-run nonces and token-based message filtering. PlaygroundClient migrated to runPlaygroundInSandbox. Tests verify iframe sandbox, CSP, and message filtering.
Documentation, migration guide, CHANGELOG, and i18n updates
CHANGELOG.md, README.md, SECURITY.md, docs/api.md, docs/migration-guide.md, docs-web/content/docs/*, docs/i18n/README.*.md, docs-web/tests/current-docs-content.test.mjs
All hardening behaviors reflected across CHANGELOG, SECURITY.md, API docs, CLI/distributed/integrations/layers/observability/migration MDX, and i18n READMEs. Docs content tests assert new strings (bypass implicit caching, allowLegacyPlaintext, layercache.key_hash).

Sequence Diagram(s)

sequenceDiagram
  participant PlaygroundClient
  participant runPlaygroundInSandbox
  participant Iframe
  participant BlobWorker
  participant MockCacheStack

  PlaygroundClient->>runPlaygroundInSandbox: run(code, onMessage, onError)
  runPlaygroundInSandbox->>Iframe: append hidden iframe (allow-scripts sandbox, CSP srcdoc)
  Iframe-->>runPlaygroundInSandbox: load event
  runPlaygroundInSandbox->>Iframe: postMessage({code, runId, messageToken, workerSource})
  Iframe->>BlobWorker: new Worker(Blob from workerSource)
  BlobWorker->>MockCacheStack: execute user code in sandboxed globals
  BlobWorker-->>Iframe: postMessage log/done (token-filtered)
  Iframe-->>runPlaygroundInSandbox: forward to parent (runId-filtered)
  runPlaygroundInSandbox-->>PlaygroundClient: onMessage(log/done) → update UI
  PlaygroundClient->>runPlaygroundInSandbox: stop()
  runPlaygroundInSandbox->>Iframe: remove from DOM, unlisten
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • flyingsquirrel0419/layercache#2: Both PRs touch src/CacheNamespace.ts—the retrieved PR introduces CacheNamespace.clear() prefix-scoped invalidation, while the main PR later adjusts CacheNamespace.clear() to invalidate with a qualified ${prefix}: key prefix (fixing delimiter-bound prefix behavior).

Suggested reviewers

  • Ingwannu
  • turin-dev
  • famomatic

Poem

🐇 Hop hop, the cache won't sneak your token away,
Plaintext rejected, symlinks kept at bay.
A hash guards your key, the namespace won't bleed,
The playground's sandboxed — no mischief shall proceed!
With --force now required when you wipe it all clean,
The rabbit signs off: security supreme! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Fix cache security hardening' accurately summarizes the primary objective of implementing comprehensive cache security hardening across the codebase, including sensitive query parameter handling, namespace boundaries, DiskLayer protection, and other security fixes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security-hardening-docs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs-web/content/docs/migration.mdx`:
- Around line 34-36: The "Implicit HTTP cache keys" section in the migration
guide references "common sensitive query parameters" without providing the
complete enumeration or linking to where they are documented. Add either a
reference link to the integrations.mdx Express middleware Options section where
the full sensitive parameter list is specified (access_token, api_key, apikey,
auth, authorization, code, credentials, id_token, jwt, password, private_key,
refresh_token, secret, session, sessionid, session_id, token), or include the
complete list directly in this migration section to ensure users can understand
the full scope of parameters being handled without needing to consult additional
documentation.

In `@src/integrations/httpCacheKeys.ts`:
- Around line 44-48: The catch block that handles URL parsing failures currently
returns hasSensitiveQuery: false, which allows implicit caching when URL
inspection fails and creates a security risk. Change the return value of
hasSensitiveQuery from false to true in the error handler so that requests where
URL parsing fails are treated as sensitive by default, ensuring the middleware
bypasses implicit caching as a fail-closed security measure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4706502d-ef4a-4e7f-ad60-df4d36002ddb

📥 Commits

Reviewing files that changed from the base of the PR and between e5107fe and e5629b6.

📒 Files selected for processing (46)
  • CHANGELOG.md
  • README.md
  • SECURITY.md
  • docs-web/components/playground/PlaygroundClient.tsx
  • docs-web/content/docs/api.mdx
  • docs-web/content/docs/cli.mdx
  • docs-web/content/docs/distributed.mdx
  • docs-web/content/docs/index.mdx
  • docs-web/content/docs/integrations.mdx
  • docs-web/content/docs/layers.mdx
  • docs-web/content/docs/migration.mdx
  • docs-web/content/docs/observability.mdx
  • docs-web/content/index.mdx
  • docs-web/content/playground.mdx
  • docs-web/lib/playground/worker-sandbox.ts
  • docs-web/lib/playground/worker.ts
  • docs-web/tests/current-docs-content.test.mjs
  • docs/api.md
  • docs/i18n/README.es.md
  • docs/i18n/README.ja.md
  • docs/i18n/README.ko.md
  • docs/i18n/README.zh-CN.md
  • docs/migration-guide.md
  • src/CacheNamespace.ts
  • src/CacheStack.ts
  • src/cli.ts
  • src/integrations/express.ts
  • src/integrations/hono.ts
  • src/integrations/httpCacheKeys.ts
  • src/integrations/opentelemetry.ts
  • src/internal/CacheKeyDiscovery.ts
  • src/internal/CacheSnapshotFile.ts
  • src/internal/PayloadProtection.ts
  • src/invalidation/RedisInvalidationBus.ts
  • src/layers/DiskLayer.ts
  • tests/CacheNamespace.test.ts
  • tests/cli.test.ts
  • tests/docs/PlaygroundWorkerSandbox.test.ts
  • tests/features/GrowthFeatures.test.ts
  • tests/integrations/Integrations.test.ts
  • tests/internal/CacheKeyDiscovery.test.ts
  • tests/internal/CacheSnapshotFile.test.ts
  • tests/internal/CacheStackInternals.test.ts
  • tests/internal/PayloadProtection.test.ts
  • tests/invalidation/RedisInvalidationBus.test.ts
  • tests/layers/DiskLayer.test.ts

Comment thread docs-web/content/docs/migration.mdx
Comment thread src/integrations/httpCacheKeys.ts
@flyingsquirrel0419
flyingsquirrel0419 force-pushed the security-hardening-docs branch from 8ae9bd7 to ee9249e Compare June 19, 2026 07:12
@flyingsquirrel0419

flyingsquirrel0419 commented Jun 20, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review please

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

@flyingsquirrel0419 Sure! I'll kick off a review of this PR now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

@flyingsquirrel0419 Sure! I'll kick off a review of this PR now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@flyingsquirrel0419 flyingsquirrel0419 self-assigned this Jun 20, 2026
@flyingsquirrel0419

Copy link
Copy Markdown
Owner Author

@coderabbitai review please

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

@flyingsquirrel0419 Sure! I'll kick off a full review of this PR right away.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@flyingsquirrel0419

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/internal/CacheKeyDiscovery.ts (2)

14-22: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Forward maxMatches to enable early termination and prevent redundant limit checking.

The maxMatches parameter is not passed to forEachKeyWithPrefix on line 16, which defeats the streaming optimization. The internal visit function in forEachKeyWithPrefix is designed to enforce the limit during key discovery, but without the parameter, it uses the default false (no limit). This causes:

  1. Memory waste: When the tag index uses non-streaming APIs (keysForPrefix/matchPattern), all matching keys are materialized into an array before any limit check.
  2. CPU waste: Layer scanning continues even after the limit is logically exceeded.
  3. Redundant checking: The visitor callback checks the limit again, but this happens too late.

The visitor check on line 18 also becomes redundant if maxMatches is properly forwarded, since the internal visit function will throw before the visitor is called for the exceeding key.

⚡ Proposed fix
 async collectKeysWithPrefix(prefix: string, maxMatches: number | false = false): Promise<string[]> {
   const matches = new Set<string>()
-  await this.forEachKeyWithPrefix(prefix, (key) => {
+  await this.forEachKeyWithPrefix(prefix, (key) => {
     matches.add(key)
-    this.assertWithinMatchLimit(matches, maxMatches)
-  })
+  }, maxMatches)

   return [...matches]
 }

Note: The assertWithinMatchLimit call in the visitor becomes unreachable and can be removed, since the internal visit function will throw before the visitor is called when the limit is exceeded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/internal/CacheKeyDiscovery.ts` around lines 14 - 22, The
`collectKeysWithPrefix` method is not forwarding the `maxMatches` parameter to
the `forEachKeyWithPrefix` call, which prevents early termination and causes
inefficient resource usage. Pass the `maxMatches` parameter directly to the
`forEachKeyWithPrefix` method so that the internal `visit` function can enforce
the limit during key discovery. Additionally, remove the now-redundant
`assertWithinMatchLimit` call from the visitor callback since the internal visit
function will throw before the visitor callback is invoked when the limit is
exceeded.

137-141: ⚠️ Potential issue | 🟡 Minor

Remove unused private method assertWithinMatchCount.

The assertWithinMatchCount method at lines 137–141 is defined but never called anywhere in the codebase. Since it's marked private, it cannot be invoked from outside the class, making this a safe removal.

Cleanup
-  private assertWithinMatchCount(matches: number, maxMatches: number | false): void {
-    if (maxMatches !== false && matches > maxMatches) {
-      throw new Error(`Invalidation matched too many keys (${matches} > ${maxMatches}).`)
-    }
-  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/internal/CacheKeyDiscovery.ts` around lines 137 - 141, Remove the unused
private method `assertWithinMatchCount` from the CacheKeyDiscovery class. This
method is never called anywhere in the codebase and can be safely deleted.
Simply remove the entire method definition including the method signature and
its body.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/internal/CacheKeyDiscovery.ts`:
- Around line 14-22: The `collectKeysWithPrefix` method is not forwarding the
`maxMatches` parameter to the `forEachKeyWithPrefix` call, which prevents early
termination and causes inefficient resource usage. Pass the `maxMatches`
parameter directly to the `forEachKeyWithPrefix` method so that the internal
`visit` function can enforce the limit during key discovery. Additionally,
remove the now-redundant `assertWithinMatchLimit` call from the visitor callback
since the internal visit function will throw before the visitor callback is
invoked when the limit is exceeded.
- Around line 137-141: Remove the unused private method `assertWithinMatchCount`
from the CacheKeyDiscovery class. This method is never called anywhere in the
codebase and can be safely deleted. Simply remove the entire method definition
including the method signature and its body.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a324ac33-a93f-4c03-8a25-ab81e2c68fc5

📥 Commits

Reviewing files that changed from the base of the PR and between b6a44dc and ca21b8a.

📒 Files selected for processing (5)
  • src/internal/CacheKeyDiscovery.ts
  • src/internal/CacheSnapshotFile.ts
  • src/internal/CacheStackSnapshotManager.ts
  • tests/internal/CacheKeyDiscovery.test.ts
  • tests/internal/CacheSnapshotFile.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/internal/CacheStackSnapshotManager.ts
  • tests/internal/CacheSnapshotFile.test.ts
  • src/internal/CacheSnapshotFile.ts

@flyingsquirrel0419
flyingsquirrel0419 merged commit 173b34d into main Jul 16, 2026
3 of 4 checks passed
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.

2 participants