Skip to content

feat(apphost): own store discovery — GenericStoreService + controller base (ADR-080) - #2310

Merged
rubenvdlinde merged 3 commits into
developmentfrom
feat/adr-079-080-store-and-settings
Aug 3, 2026
Merged

feat(apphost): own store discovery — GenericStoreService + controller base (ADR-080)#2310
rubenvdlinde merged 3 commits into
developmentfrom
feat/adr-079-080-store-and-settings

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

A "store" is a remote OpenRegister instance exposing installable items over its objects API. OpenRegister is already the server of that protocol; this makes it the client too, instead of each app growing its own proxy.

What lands

  • AppHost/Service/GenericStoreServiceisConfigured / search / resolve, the SSRF guard, redirect refusal, bounded timeouts, Bearer-only token use, four-outcome contract
  • AppHost/Service/StoreDescriptor — per-app parameters (appId, schema, defaultRegister, cardFields)
  • AppHost/Controller/GenericStoreControllerBase — auth guard, slug validation, error envelope

Generalised from openbuild's RemoteTemplateStoreService, which this replaces (see the companion openbuild PR).

Install is deliberately absent

Cloning an application template, enabling a connector adapter and instantiating an agent template are different operations with different authorization. Each app keeps its own install action and calls resolve() for the payload — the ADR-080 Decision 3 seam.

The guard is now a direct call

openbuild reached SecurityService::assertSafeFetchUrl() through a dynamic class-string with a weaker local fallback if the class was missing. Reasonable there; unnecessary here, and one fewer degraded path to get wrong.

Worth reading the test file for

The first run had every happy path returning store_unreachable, because assertSafeFetchUrl resolves DNS and fails closed, and the fixture hostname does not resolve. The SSRF negative controls were passing for the wrong reason — they only proved that everything was rejected.

They now use literal public IPs so the positive control is real and offline, and a dedicated test pins the fail-closed-on-unresolvable behaviour so the next reader does not rediscover it.

Also asserted: a card never carries the install payload (no manifest, no token), and resolve() trusts the returned slug rather than the filter, so a registry ignoring an unknown query param cannot hand back an arbitrary first row.

Verification

14/14 new tests · full AppHost suite 167/167.

Purely additive — three new classes plus one test file, no existing file touched.

Companion PRs: hydra (ADR-079/080), nextcloud-vue, openbuild, openconnector.

… base (ADR-080)

A "store" is a remote OpenRegister instance exposing installable items over its
objects API. OpenRegister is already the SERVER of that protocol; this makes it
the client too, instead of each app growing its own proxy.

Generalised from openbuild's RemoteTemplateStoreService, parameterised by a
per-app StoreDescriptor (appId, schema, defaultRegister, cardFields). The
engine owns, once: isConfigured/search/resolve, the SSRF guard, redirect
refusal, bounded timeouts, Bearer-only token use, and the four-outcome contract.

INSTALL is deliberately absent. Cloning an application template, enabling a
connector adapter and instantiating an agent template are different operations
with different authorization, so each app keeps its own install action and calls
resolve() for the payload. That is the ADR-080 Decision 3 seam.

The guard is now a direct SecurityService::assertSafeFetchUrl() call. openbuild
reached it through a dynamic class-string with a weaker local fallback if the
class was missing — reasonable there, unnecessary here, and one fewer degraded
path to get wrong.

On the tests: the first run had every happy path returning store_unreachable,
because assertSafeFetchUrl resolves DNS and fails CLOSED, and the fixture
hostname does not resolve. The SSRF negative controls were passing for the wrong
reason — they only proved that EVERYTHING was rejected. They now use literal
public IPs so the positive control is real and offline, and a dedicated test
pins the fail-closed-on-unresolvable behaviour so the next reader does not
rediscover it. 14/14; full AppHost suite 167/167.
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ f6e0a41

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
composer ✅ 174/174
npm ✅ 713/713
PHPUnit ⏭️
Newman ⏭️
Playwright

Quality workflow — 2026-08-03 18:53 UTC

Download the full PDF report from the workflow artifacts.

…ervice, consumed by composition (ADR-080)

A cross-app abstract controller broke in three places: NC's router reflects
every controller during route MATCHING (absent OR = 500 on every route in the
consuming app), leaf test suites stub OR rather than autoloading it so the
subclass could not load, and phpstan rejects 'extends unknown class' and refuses
to let it be ignored. All three come from `extends` being resolved by the
autoloader rather than the container.

GenericStoreService stays and is injected. A service type-hint has none of those
problems and is the shape leaf apps already use for ObjectService.
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ ad67f99

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
composer ✅ 174/174
npm ✅ 713/713
PHPUnit ⏭️
Newman ⏭️
Playwright

Quality workflow — 2026-08-03 19:03 UTC

Download the full PDF report from the workflow artifacts.

…phpmd

fetch() sat exactly at the cyclomatic threshold (10) and over NPath (216 vs
200). Splitting the body decode out is not just a metric fix — an unparseable
body is a DIFFERENT outcome from an unreachable registry, and having the two
guard chains in one method is what made them easy to collapse.

StaticAccess on SecurityService::assertSafeFetchUrl is suppressed with a
reason: it is static upstream, and calling it directly is the point of moving
this client into OpenRegister (the app-local copy reached it via a dynamic
class-string with a weaker fallback).
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ ac9c339

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
composer ✅ 174/174
npm ✅ 713/713
PHPUnit
Newman
Playwright

Quality workflow — 2026-08-03 19:16 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 7dc86cd into development Aug 3, 2026
29 checks passed
@rubenvdlinde
rubenvdlinde deleted the feat/adr-079-080-store-and-settings branch August 3, 2026 19:18
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