Skip to content

feat: expose urlPath in deploy_component operation and CLI - #1113

Merged
kriszyp merged 4 commits into
mainfrom
kris/deploy-urlpath
Jun 8, 2026
Merged

feat: expose urlPath in deploy_component operation and CLI#1113
kriszyp merged 4 commits into
mainfrom
kris/deploy-urlpath

Conversation

@kriszyp

@kriszyp kriszyp commented Jun 3, 2026

Copy link
Copy Markdown
Member

Summary

Exposes the urlPath component config option (added in #397) as a first-class parameter of the deploy_component operation and CLI.

  • urlPath is accepted by the operation validator and persisted in harperdb-config.yaml alongside package for package-based component deployments
  • CLI works for free via the existing generic key=value argument parser: harper deploy project=my-app package=@my/pkg urlPath=/api
  • urlPath is rejected without package (it's only meaningful in the package-based config path; payload-deployed components configure urlPath in their own harper-config.yaml)
  • urlPath containing .. or empty strings are rejected at the API boundary before reaching disk

How it flows

For package-based components the root harperdb-config.yaml entry (e.g. my-app: { package: "...", urlPath: "/api" }) is what OptionsWatcher reads and scopes on. That value then flows into scope.options.getAll().urlPath and into the per-route middleware chain from #397.

Known limitation (pre-existing, not introduced here)

addConfig does a full setIn replace on the component's config block. Re-deploying without specifying urlPath will overwrite the existing value — the same behaviour applies today for install_command and other persisted fields.

Tests

Added deployComponentValidator unit tests covering the accept, reject-without-package, path-traversal, empty-string, and missing-project cases.

Generated by Claude Sonnet 4.6 🤖

kriszyp and others added 3 commits June 2, 2026 21:37
Components registered via package can now set urlPath at deploy time.
The value is persisted in the root config alongside package/install,
where the OptionsWatcher already reads and scopes it per component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rejects urlPath containing '..' at the API boundary (prevents invalid
  config being written to disk before downstream validation fires)
- Rejects empty urlPath strings
- Requires package when urlPath is present (urlPath is only persisted in
  the package-based config path; payload deployments set it in their own
  harper-config.yaml)
- Adds deployComponentValidator unit tests covering accept, reject, and
  path-traversal cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Avoids false-positive if validateBySchema returns undefined (success).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@kriszyp
kriszyp requested a review from a team June 3, 2026 04:04
@claude

claude Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@kriszyp
kriszyp marked this pull request as ready for review June 4, 2026 01:41
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@kriszyp
kriszyp merged commit 02dcf72 into main Jun 8, 2026
39 of 42 checks passed
@kriszyp
kriszyp deleted the kris/deploy-urlpath branch June 8, 2026 21:27
kriszyp added a commit that referenced this pull request Jul 31, 2026
Where an application is served is a deployment concern, not an application concern, but
`host`/`urlPath` were only readable from the config file that declared a plugin. For an
application that is its own `config.yaml`, so the hostname and mount point had to be
checked into the app — unoverridable from outside it (the env-config overlay is
root-config-only).

Worse, `host`/`urlPath` on a root-config *application* entry were silently inert. An
application's plugin scopes read the application's own config.yaml and nothing carried the
root entry's routing down to them, so `deploy_component urlPath=/api` (#1113) persisted a
value that changed nothing. The flow described in #1113 only ever held for a root-declared
*plugin*, whose scope does read the root config.

The root config is now authoritative for where an application is served:

    my-app:
      host: api.example.com
      urlPath: /v1

- `scopeMount.ts` — pure mount model. `host` is replaced outright (an operator remapping a
  hostname must win over a value the app shipped). `urlPath` is composed rather than
  replaced, because a plugin's `urlPath` doubles as its app-internal base path (static's
  asset root, fastify's route prefix); replacing it would silently relocate app-internal
  URLs and collapse distinct plugins onto one path. Mount `/v1` + `static: { urlPath:
  assets }` → `/v1/assets/`. The composed value is a fixed point of `resolveBaseURLPath`,
  so downstream consumers keep resolving it without compounding the prefix.
- Overlaid in `OptionsWatcher`, not at each call site, so `scope.options.getAll()` is the
  one effective view of a plugin's config. static's redirects and external paths, the
  EntryHandler's entry URLs, and fastify's route prefix are all correct with no changes of
  their own. Composed from the freshly-parsed file on every read, so live reload cannot
  compound the prefix.
- Applied on both load paths: the root-config `package` recursion and the components-root
  directory scan. The scan is the path that matters most — it loads apps with no root
  entry at all, so a mount works for a payload-deployed app, not just an installed one.
- `deploy_component` accepts and persists `host` alongside `urlPath`, rejecting a host
  that carries a port or path (it would never match the router's host compare).

Also fixes the Scope `server` proxy passing a raw config `urlPath` straight to the router:
a plugin that spreads its whole config section into these options (REST does) handed the
router the literal './', which normalized to the unmatchable route '/.'. The proxy now
resolves whichever source supplied the value.

Documented in HarperFast/documentation#595.
kriszyp added a commit that referenced this pull request Jul 31, 2026
Where an application is served is a deployment concern, not an application concern, but
`host`/`urlPath` were only readable from the config file that declared a plugin. For an
application that is its own `config.yaml`, so the hostname and mount point had to be
checked into the app — unoverridable from outside it (the env-config overlay is
root-config-only).

Worse, `host`/`urlPath` on a root-config *application* entry were silently inert. An
application's plugin scopes read the application's own config.yaml and nothing carried the
root entry's routing down to them, so `deploy_component urlPath=/api` (#1113) persisted a
value that changed nothing. The flow described in #1113 only ever held for a root-declared
*plugin*, whose scope does read the root config.

The root config is now authoritative for where an application is served:

    my-app:
      host: api.example.com
      urlPath: /v1

- `scopeMount.ts` — pure mount model. `host` is replaced outright (an operator remapping a
  hostname must win over a value the app shipped). `urlPath` is composed rather than
  replaced, because a plugin's `urlPath` doubles as its app-internal base path (static's
  asset root, fastify's route prefix); replacing it would silently relocate app-internal
  URLs and collapse distinct plugins onto one path. Mount `/v1` + `static: { urlPath:
  assets }` → `/v1/assets/`. The composed value is a fixed point of `resolveBaseURLPath`,
  so downstream consumers keep resolving it without compounding the prefix.
- Overlaid in `OptionsWatcher`, not at each call site, so `scope.options.getAll()` is the
  one effective view of a plugin's config. static's redirects and external paths, the
  EntryHandler's entry URLs, and fastify's route prefix are all correct with no changes of
  their own. Composed from the freshly-parsed file on every read, so live reload cannot
  compound the prefix.
- Applied on both load paths: the root-config `package` recursion and the components-root
  directory scan. The scan is the path that matters most — it loads apps with no root
  entry at all, so a mount works for a payload-deployed app, not just an installed one.
- `deploy_component` accepts and persists `host` alongside `urlPath`, rejecting a host
  that carries a port or path (it would never match the router's host compare).

Also fixes the Scope `server` proxy passing a raw config `urlPath` straight to the router:
a plugin that spreads its whole config section into these options (REST does) handed the
router the literal './', which normalized to the unmatchable route '/.'. The proxy now
resolves whichever source supplied the value.

Documented in HarperFast/documentation#595.
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