Skip to content

Conversation

@xeno097
Copy link
Contributor

@xeno097 xeno097 commented Dec 8, 2025

Description

Adds a read only registry for environments where it is desirable not to write back to the underlying registry implementation

Backward compatibility

Testing

  • unit

Summary by CodeRabbit

  • New Features
    • ReadonlyRegistry wrapper added to provide read-only access and block modifications; readonly registry type introduced.
    • Attempts to write to a readonly registry now produce logged notifications.
  • Tests
    • Unit tests added to validate readonly behavior, read delegation, blocking of write attempts, and merging with other registries.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Dec 8, 2025

🦋 Changeset detected

Latest commit: e23e651

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hyperlane-xyz/registry Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 8, 2025

📝 Walkthrough

Walkthrough

This change adds a ReadonlyRegistry wrapper class exposing read-only behavior by delegating reads to an inner registry and blocking write methods. It also introduces an IRegistryWriteMethod type and a RegistryType.Readonly enum member, plus tests and a version bump.

Changes

Cohort / File(s) Change Summary
Registry Type Definitions
src/registry/IRegistry.ts
Introduced IRegistryWriteMethod type, refactored base/read/write registry interfaces (IBaseRegistry, IReadRegistry, IWriteRegistry), added warp-read methods to IReadRegistry, removed them from IWriteRegistry, added RegistryType.Readonly, and defined composite IRegistry = IReadRegistry & IWriteRegistry & { merge(...) }.
ReadonlyRegistry Implementation
src/registry/ReadonlyRegistry.ts
Added ReadonlyRegistry class: holds innerRegistry, type = RegistryType.Readonly, delegates all read methods to inner registry, intercepts/blocks write methods via skipMethodExecution (logs and no-ops), exposes getUri, listRegistryContent, warp read helpers, and merge() returning a ReadonlyRegistry wrapping a MergedRegistry.
Tests
test/unit/registry.test.ts
Added tests for ReadonlyRegistry: verifies type, delegation of read ops, write ops are blocked and logged, integration with merged registry, and extended display-name expectations.
Release / Changelog
.changeset/strange-apricots-search.md
Bumped package minor version and documented the ReadonlyRegistry export and API additions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Pay extra attention to:
    • src/registry/ReadonlyRegistry.ts — ensure every write method is intercepted and logging behavior is consistent.
    • src/registry/IRegistry.ts — confirm IRegistryWriteMethod accurately enumerates writable method names and interface split doesn't miss any public surface.
    • test/unit/registry.test.ts — validate tests assert both delegation and blocking behaviors and that typings line up.

Possibly related PRs

Suggested reviewers

  • ltyu

Poem

In the swamp of code, a gentle guard stands by,
Reads what you whisper, but won't let writes fly.
A wrapper polite, with a logger to say,
"Not today, laddie" — no changes this way. 🐸✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The description covers the main purpose but leaves the backward compatibility section empty, which is a required template field that should be explicitly answered. Complete the backward compatibility section by explicitly stating 'Yes' or 'No' and provide a brief explanation of how this change affects existing code.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: read only registry' clearly and concisely summarizes the main change—introducing a ReadonlyRegistry wrapper to prevent write operations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch xeno/read-only-registry

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/registry/ReadonlyRegistry.ts (1)

86-90: Consider using warn level instead of info for blocked operations.

When someone tries to write to a readonly registry, that's usually not the expected path - might be worth logging at warn level so it stands out more in logs. Currently using info might make these calls blend in with normal operations, and folks might miss that their writes are being silently swallowed.

That said, if the intention is that this is expected behavior in your use cases (like exposing to untrusted code where writes are anticipated and intentionally ignored), info is fine. Just something to chew on.

 private skipMethodExecution(methodName: IRegistryWriteMethod): MaybePromise<void> {
   const logger = this.logger ?? console;

-  logger.info(`Skipping "${methodName}" method call as registry is readonly`);
+  logger.warn(`Skipping "${methodName}" method call as registry is readonly`);
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1783373 and 7f59e88.

📒 Files selected for processing (4)
  • .changeset/strange-apricots-search.md (1 hunks)
  • src/registry/IRegistry.ts (2 hunks)
  • src/registry/ReadonlyRegistry.ts (1 hunks)
  • test/unit/registry.test.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
test/unit/registry.test.ts (3)
src/registry/ReadonlyRegistry.ts (1)
  • ReadonlyRegistry (46-137)
src/registry/IRegistry.ts (1)
  • IRegistryWriteMethod (50-52)
src/registry/PartialRegistry.ts (1)
  • PartialRegistry (35-130)
src/registry/ReadonlyRegistry.ts (3)
src/index.ts (1)
  • RegistryType (13-13)
src/types.ts (2)
  • MaybePromise (10-10)
  • WarpDeployConfigMap (42-42)
src/registry/IRegistry.ts (1)
  • IRegistryWriteMethod (50-52)
🪛 ast-grep (0.40.0)
test/unit/registry.test.ts

[warning] 373-373: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(Skipping "${methodName}" method call as registry is readonly)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (13)
.changeset/strange-apricots-search.md (1)

1-5: Aye, looks good to me!

Minor version bump is the right call for adding a new feature like this. The description captures what this is all about - a registry that won't let anyone muck about with the data.

src/registry/IRegistry.ts (2)

29-52: Nice bit of type magic here, well done!

The satisfies Record<IRegistryMethods, boolean> is a proper smart move - if someone adds a new method to IRegistry but forgets to add it to this map, TypeScript will start complaining like a donkey. Keeps everything in sync without any extra effort.

The derived IRegistryWriteMethod type is clean and will catch any misuse at compile time.


78-78: New enum value fits right in.

The Readonly registry type slots in nicely with the existing types. No complaints here.

test/unit/registry.test.ts (5)

70-77: Good to see the readonly wrapper getting a proper seat at the table.

Including readonlyRegistry in the main test loop ensures it behaves like any proper registry for read operations. Smart way to get broad coverage without duplicating tests.


89-97: Display name test now accounts for the readonly type - nice catch.

The condition properly includes RegistryType.Readonly since it wraps the merged registry which has the partial overlay.


294-366: This test structure is as robust as an ogre's constitution.

Using Record<IRegistryWriteMethod, ...> for the test cases is clever - if a new write method gets added to the interface but not to these tests, TypeScript will let you know right quick. Exhaustive testing without the manual bookkeeping.


368-377: About that static analysis warning - it's a false alarm.

The methodName here comes from the IRegistryWriteMethod type which is a fixed set of known string literals ('addChain', 'updateChain', etc.). It's not user input, so there's no ReDoS risk. The analyzer just doesn't have the context to know these are safe, controlled values.


379-402: Read delegation and merge behavior tests are solid.

Good coverage of the core functionality - reads go through, and merging preserves the readonly nature. The assertion that merged is an instanceOf ReadonlyRegistry is exactly what we need to verify the wrapper behavior is maintained.

src/registry/ReadonlyRegistry.ts (5)

26-44: The documentation is like layers on an onion - helpful and well-structured.

Good example usage and clear explanation of when to use this. Helps other developers understand the purpose without having to dig through the implementation.


46-52: Constructor is clean and simple.

Storing the inner registry and copying over the URI makes sense. The readonly modifiers on constructor parameters are a nice touch for immutability.


131-136: Merge keeps the readonly wrapper - exactly right.

Wrapping the MergedRegistry in a new ReadonlyRegistry ensures the readonly guarantee isn't lost when combining registries. Good defensive design.


54-84: Read method delegation is straightforward and complete.

All the read operations just pass through to the inner registry. Nothing fancy, nothing to break - exactly how a wrapper should work.


92-121: Write methods properly blocked with type-safe method names.

Each write method passes its name to skipMethodExecution, and TypeScript ensures we can only pass valid IRegistryWriteMethod values. Clean and maintainable.

warpRouteId: WarpRouteId;
};

export interface IRegistry {
Copy link
Member

Choose a reason for hiding this comment

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

did you consider separating this interface into IReadRegistry and IWriteRegistry and IRegistry is IReadRegistry, IWriteRegistry?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, the reason for not doing it, in the even if the ReadonlyRegistry will be used in specific contexts, is that imo it should still be possible to use it wherever a IRegistry might be used. If it implemented only a subset of the IRegistry interface methods, we wouldn't be able to use it everywhere and additionally client code (sdk, cli ...) should be aware of the new interface and conditionally call methods based on the interface type which would be a much bigger change.

Copy link
Member

Choose a reason for hiding this comment

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

as suggested, we could maintain backward compatibility by having the IRegistry be the union of the two interfaces
I think its desirable for the client to only have access to the read methods when using the read interface

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sounds good to me

Comment on lines +92 to +98
addChain(_chain: UpdateChainParams): MaybePromise<void> {
return this.skipMethodExecution('addChain');
}

updateChain(_chain: UpdateChainParams): MaybePromise<void> {
return this.skipMethodExecution('updateChain');
}
Copy link
Member

Choose a reason for hiding this comment

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

imo it makes sense to encode this in the type and catch this at compile time rather than have an exception at runtime

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we wouldn't have an exception because the skipMethodExecution would only log the info message that the call will be skipped as the current registry is a readonly one

Copy link
Member

Choose a reason for hiding this comment

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

that seems possibly worse to "silently" skip the operation requested by the client?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems a pretty common pattern in the registry code as we already do something similar in the merged registry (not saying it is correct, I'd be in favor of changing it later to be more explicit tbh)

  • protected async multiRegistryWrite(
    writeFn: (registry: IRegistry) => Promise<void>,
    methodName: IRegistryMethods,
    logMsg: string,
    ): Promise<void> {
    for (const registry of this.registries) {
    if (registry.unimplementedMethods?.has(methodName)) {
    this.logger.warn(`Skipping ${logMsg} at ${registry.type} registry (not supported)`);
    continue;
    }
    try {
    this.logger.info(`Now ${logMsg} at ${registry.type} registry at ${registry.uri}`);
    await writeFn(registry);
    this.logger.info(`Done ${logMsg} at ${registry.type} registry`);
    } catch (error) {
    // To prevent loss of artifacts, MergedRegistry write methods are failure tolerant
    this.logger.error(`Failure ${logMsg} at ${registry.type} registry`, error);
    }
    }
    }
  • async updateChain(chain: UpdateChainParams): Promise<void> {

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/registry/IRegistry.ts (1)

66-76: Clean IBaseRegistry / IReadRegistry split; minor typing polish you might consider

The split into IBaseRegistry plus a read‑only surface (IReadRegistry) lines up nicely with the earlier design discussion and keeps unimplementedMethods centralized. Structurally this reads well and should make it straightforward to plug in a readonly implementation that only exposes reads.

Two small nits you may want to consider, nothing blocking:

  • getWarpRoute / getWarpDeployConfig currently take routeId: string; if you want tighter typing to match how you key maps and options, using WarpRouteId here (like in AddWarpRouteConfigOptions) would keep things a bit more coherent.
  • The TODOs for “more deployment artifact related methods” are duplicated across read/write; if they’re meant to diverge (read vs write concerns), you might eventually want slightly more specific wording or separate TODOs so future you doesn’t get lost in the swamp.

Also applies to: 77-99

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f59e88 and e23e651.

📒 Files selected for processing (1)
  • src/registry/IRegistry.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/registry/IRegistry.ts (2)
src/types.ts (5)
  • MaybePromise (10-10)
  • WarpRouteFilterParams (29-29)
  • WarpRouteConfigMap (41-41)
  • WarpDeployConfigMap (42-42)
  • UpdateChainParams (36-38)
src/index.ts (5)
  • WarpRouteFilterParams (29-29)
  • WarpRouteConfigMap (28-28)
  • UpdateChainParams (31-31)
  • AddWarpRouteConfigOptions (9-9)
  • IRegistry (11-11)
🔇 Additional comments (2)
src/registry/IRegistry.ts (2)

55-55: RegistryType.Readonly enum member looks good; mind exhaustiveness checks

The new Readonly = 'readonly' fits the existing enum pattern just fine. Only thing to watch is any switch/if ladders over RegistryType that assume the old set; they may need a new branch for the readonly case depending on how strict the handling is.


101-113: IWriteRegistry + composite IRegistry type keep the public surface intact

The write surface hanging off IWriteRegistry (chain mutations + warp additions) looks straightforward, and redefining IRegistry as IReadRegistry & IWriteRegistry & { merge(...) } keeps the overall contract essentially the same while allowing the readonly flavor to implement only what it cares about and mark the rest as unimplemented.

From a consumer’s point of view, classes can still implements IRegistry just fine, and merge(otherRegistry: IRegistry): IRegistry keeps the type story clear for registry composition.

Also applies to: 115-118

*/
export type IRegistryMethods = MethodsOf<Omit<IRegistry, 'unimplementedMethods'>>;

export type IRegistryWriteMethod = keyof IWriteRegistry;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Tighten IRegistryWriteMethod to only include method keys

IRegistryWriteMethod = keyof IWriteRegistry includes non-function properties (type, uri, unimplementedMethods) alongside the actual mutating methods. This creates a type mismatch when used with Set<IRegistryMethods> as intended for the unimplementedMethods field.

Use MethodsOf<IWriteRegistry> instead to filter to function-valued keys only:

-export type IRegistryWriteMethod = keyof IWriteRegistry;
+export type IRegistryWriteMethod = MethodsOf<IWriteRegistry>;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type IRegistryWriteMethod = keyof IWriteRegistry;
export type IRegistryWriteMethod = MethodsOf<IWriteRegistry>;
🤖 Prompt for AI Agents
In src/registry/IRegistry.ts around line 29, the type alias IRegistryWriteMethod
is currently keyof IWriteRegistry which includes non-function properties and
causes a mismatch when used with Set<IRegistryMethods>; change it to use
MethodsOf<IWriteRegistry> (or the project’s equivalent utility type that selects
only function-valued keys) so IRegistryWriteMethod represents only method keys,
and update any imports/exports accordingly to reference MethodsOf.

Comment on lines +116 to +118
IWriteRegistry & {
merge(otherRegistry: IRegistry): IRegistry;
};
Copy link
Member

Choose a reason for hiding this comment

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

maybe you can put this on the BaseRegistry?

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.

3 participants