Skip to content

feat(registry): add onComponentRequest hook - #1602

Merged
ricardo-devis-agullo merged 3 commits into
masterfrom
feat/component-request-hook
Nov 9, 2026
Merged

ricardo-devis-agullo merged 3 commits into
masterfrom
feat/component-request-hook

Conversation

@ricardo-devis-agullo

@ricardo-devis-agullo ricardo-devis-agullo commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds an onComponentRequest registry option so registry owners can apply their own policy to component requests after OC has resolved the version. The motivating case is rejecting outdated versions for security reasons. That policy belongs to each registry, not to OC, so this adds a generic hook rather than built-in version rules.

oc.Registry({
  // ...
  onComponentRequest: ({ version, latestVersion }) => {
    if (semver.major(version) < semver.major(latestVersion)) {
      return { status: 410, code: 'VERSION_TOO_OLD', error: `Use ${latestVersion}` };
    }
  }
});
  • Context passed to the hook: name, requestedVersion, the resolved version, latestVersion, component (its package.json, including allVersions), headers, parameters and action.
  • latestVersion: uses the same rule OC applies to unversioned requests, so pre-releases are excluded. It is computed only when the hook reads it, and the result is cached per component version list (a WeakMap keyed on the list held by the components cache). It is recalculated only when that list changes after a publish or poll, which takes a read from ~190 µs to ~9 ns at 500 versions. Unversioned requests use the same cache, so they also stop recalculating the latest version on every request.
  • Return value: returning nothing lets the request through. Returning { status?, code?, error? } rejects it; the defaults are 403 and COMPONENT_REQUEST_REJECTED. The hook may also return a Promise.
  • Errors: if the hook throws or its Promise rejects, the request gets a 500 with code COMPONENT_REQUEST_HOOK_FAILED, and an error event is fired. Requests are not let through when the hook fails.
  • Where it runs: in get-component.ts, so it covers GET /:name/:version, batch POST /, actions, nested renderComponent calls and accept: application/vnd.oc.info+json requests. If no hook is configured, nothing changes.
  • Config validation: the option must be a function.
  • Nested error messages: when a data provider calls back with a string error, which is what a rejected nested renderComponent produces, that string is now included in the error message. Previously the message was Component execution error: with nothing after it. details is unchanged.

Not covered (open for discussion)

  • /:name/:version/~info, ~preview and versioned static assets (component-static.ts) don't use get-component.ts, so the hook doesn't run for them.
  • Components served through fallbackRegistryUrl don't go through the hook, because no local package.json is available for them.

Test plan

  • npm run build (biome + tsc)
  • npm run test-silent: 1175 passing
  • Unit tests (registry-routes-component.js): context contents, pass-through, sync and async rejection, default values, sync throw, async rejection, info-header requests
  • Acceptance tests (registry.js): a real registry rejects on GET, rejects only the matching item in a batch POST /, and runs the hook for nested renders
  • Validator tests for a non-function value
  • version-handler and repository tests for the cached latest version, including that changes to the per-request allVersions copy don't affect it

Generated with Devin

ricardo-devis-agullo and others added 3 commits November 9, 2026 15:02
Lets registry owners apply their own per-request policy (e.g. rejecting
outdated component versions) once the requested version has been
resolved, without baking that policy into OC itself.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The hook's latestVersion getter re-ran semver over every published version
on each read. Memoise it against the components cache's version array, which
is replaced (not mutated) on publish/poll, so it is only recomputed when the
list changes. Unversioned requests reuse the same cache.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A nested renderComponent rejection (e.g. from onComponentRequest) reaches
the parent data provider as a string, which produced an empty
"Component execution error: " message.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@ricardo-devis-agullo
ricardo-devis-agullo merged commit ee113dc into master Nov 9, 2026
3 checks passed
@ricardo-devis-agullo
ricardo-devis-agullo deleted the feat/component-request-hook branch November 9, 2026 15:02
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