Skip to content

feat: Registry Service (v2) client, instance registration and installs - #44

Merged
roncodes merged 7 commits into
release/v0.2.0from
feat/registry-service-client
Oct 24, 2026
Merged

roncodes merged 7 commits into
release/v0.2.0from
feat/registry-service-client

Conversation

@roncodes

@roncodes roncodes commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Phase 1, Workstream D of the extensions registry re-architecture: the instance module reads the new Registry Service (fleetbase/registry branch v2, fleetbase/registry#4).

Includes the unpushed Phase 0 commit. This branch is based on phase-0/central-catalog (0c34582, the interim central catalog, RegistryClient and CentralCatalog), which is not on the remote, so that commit shows in this diff too. main (v0.1.11 and the sandbox live-connection fix) is merged in; RegistryServiceInstall follows that fix and is pinned to the live connection.

What changes

  • RegistryClient v2 mode. Used by default when registry.service_url is set; registry.api_version can pin v1. Catalog from GET /v1/catalog/extensions, extensions/{slug} and categories; updates from GET /v1/instances/me/updates; install reports to POST/DELETE /v1/instances/me/installs. The cloud ~registry/v1/extensions path stays the fallback without a service URL. 4xx responses no longer count towards the circuit breaker.
  • Instance registration. POST registry-extensions/instance/register (registration code) calls POST /v1/instances/register with name, host, kind, console and API versions; DELETE registry-extensions/instance forgets the registration. The service URL and the instance token are stored in core-api settings (registry-bridge.registry.*), the token encrypted with the app key and only returned masked.
  • Admin page (registry-admin-config). Service URL, register with a code, registered status with the masked token, unregister, and Test connection also checks the instance token.
  • Installs. lookup resolves package names from the service catalog. Composer authenticates with COMPOSER_AUTH; npm with NPM_CONFIG_//<host>/npm/:_authToken plus npm_config_userconfig pointing at server/resources/npm/registry.npmrc, which holds only ${…} placeholders (pnpm 11 ignores //host keys in the environment). No auth.json or .npmrc with a token is written. Engines are added by tarball URL so their npm scope is never remapped. Local registry_service_installs rows keep engines loading.
  • Explore / Installed / Purchased. Service listings are mapped into the registry-extension shape: is_installed, is_purchased (active paid licence), current_bundle_version (latest release), update_available, and meta.install_blocked_reason, shown in the details modal. Cards show an update badge.

The synchronous installer in this PR is replaced by the queued installer in the stacked PR.

Testing

  • Pest: 55 new tests on responses recorded from a running service (server/tests/Fixtures/registry-service, with a re-record script); tokens in fixtures are placeholders.
  • Live: registered an instance against a local service, installed and uninstalled the paid demo extension with real Composer 2.8 and pnpm 11, confirmed licence seat and updates data, and that no credential file was written.
  • Ember: the admin-config and extension-card integration tests were added but not confirmed green locally (machine overload); one earlier run showed a tooltip teardown error that is fixed in this branch.

Decisions needed

  1. The installer adds the service Composer repository to the API's composer.json (no secret in it).
  2. Engines are installed by tarball URL; the alternative is the service proxying unknown npm packages.
  3. Registration is required to install, even free extensions.
  4. Explore hides listings whose hosting excludes this instance's kind.
  5. self_hosted listings install through the installer, not as manual installs.
  6. The service has no self-revoke endpoint for an instance token (DELETE /v1/instances/me).
  7. Registration reports app.url as the host and the core-api version as the API version.
  8. Paid listings without a licence are shown but blocked until Phase 4 checkout.

…g test, extension.json schema

- RegistryClient (Http) with cache and circuit breaker; CentralCatalog maps the cloud
  public extensions endpoint into the RegistryExtension resource shape when
  registry.self_hosted is false; install/purchase return a clear error for remote records
- POST registry-extensions/config/test and a Test connection button; token masked with
  ClickToReveal in the admin config component
- extension.json JSON Schema (server/resources/schemas) validated in bundleUpload via
  opis/json-schema; ExtensionManifest support class
- Explore empty state component with registry-config link; category page titled by category
- Test tooling: ember-ui 0.4.6 / ember-core 0.3.26 pins, eager engine loading under
  ember test, dummy app models and intl config, host stubs in tests/helpers

Part of the Fleetbase Extensions Registry stabilisation (plan: https://claude.ai/artifact/6i2o4Qvq4HhsJje9YyqjYB).
Phase 1 Workstream D of the extensions registry re-architecture
(plan: https://claude.ai/artifact/6i2o4Qvq4HhsJje9YyqjYB).

- RegistryClient v2 mode (default when registry.service_url is set): catalog
  from /v1/catalog/*, updates from /v1/instances/me/updates, install reports to
  POST/DELETE /v1/instances/me/installs; the cloud ~registry/v1/extensions path
  stays the fallback without a service URL. 4xx no longer trips the circuit.
- Instance registration: service URL and encrypted instance token in settings;
  POST registry-extensions/instance/register and DELETE .../instance; admin
  config page gets service URL, register with code, masked token, unregister
  and a token check in Test connection.
- Installer: lookup resolves from the service catalog; Composer auth via
  COMPOSER_AUTH, npm via NPM_CONFIG_//host/npm/:_authToken plus a
  placeholder-only npmrc (pnpm 11 ignores //host keys in env); engines added by
  tarball URL; installs and uninstalls reported to the instance API; local
  registry_service_installs rows keep engines loading.
- Explore/Installed/Purchased map service listings into the
  registry-extension shape (is_installed, is_purchased, update_available,
  current_bundle_version, install_blocked_reason).
- Pest: 55 tests on responses recorded from a local service.
Comment thread tests/integration/components/registry-admin-config-test.js Fixed
Comment thread tests/integration/components/registry-admin-config-test.js Fixed
Conflicts: the categories endpoint checks the central catalog before main's
live-connection query; the lockfile is the one matching the merged
package.json (main's only package.json change is the version).
… mode

Follows main's sandbox fix: RegistryServiceInstall is pinned to the mysql
connection like the other registry models; installed() and purchased() can
be called without a request again; the sandbox harness creates
registry_service_installs, counts seven models and runs as the cloud
registry so categories take the local-tables path.
CodeQL flagged the array .includes() assertions as incomplete URL substring
sanitization (js/incomplete-url-substring-sanitization). Use an explicit
equality check so the PR's CodeQL gate passes.
D21: the registration (service URL, instance token, instance) was merged
into config only when a worker booted, so under Octane
`php artisan registry:register` (flb instance register) and the admin
endpoint left other web workers on the old registration until
octane:reload. RegistrySettings now reads the registry settings with one
query on first use through RegistryConfig::current(), once per Octane
request (reset on RequestReceived) and per queued job (reset on
JobProcessing). Keys with no stored value fall back to the config file
and environment, so a forgotten registration clears everywhere too.

D17: re-registering created a second instance on the service. The
register payload now carries the current instance token as
previous_token, sent only to the service that issued it, so the service
rotates the same instance (registry release/v2.0.0 d9c3661).
…record rotated registrations

D21 follow-up: RegistrySettings read the settings table once per Octane
request and queued job. The stored values now sit in the application cache
for REGISTRY_SETTINGS_CACHE_TTL seconds (30 by default, 0 to turn it off), so
a request costs a cache read. Writing or deleting a registry setting forgets
the cache: a Setting saved/deleted listener, plus InstanceRegistration::store()
and forget() (whose query-builder delete fires no model events). With a shared
store, a `registry:register` run in another process applies to every worker on
the next request; with a per-container store, within the TTL. An unreadable
settings table is never cached.

D17 follow-up: the service now answers `rotated` (200) when it re-issues the
token of an instance it already had (by previous token or by host, registry
68331fe). RegistryClient::registerInstance() returns it, the stored instance
summary keeps it, and the admin config payload exposes instance.rotated, so the
admin page and registry:register can say the same instance kept its installs
and seats.

README documents re-registration and the settings cache.
@roncodes
roncodes marked this pull request as ready for review October 24, 2026 09:29
@roncodes roncodes closed this Oct 24, 2026
@roncodes roncodes reopened this Oct 24, 2026
@roncodes
roncodes merged commit eafbdff into release/v0.2.0 Oct 24, 2026
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