Repository navigation
feat(frontend): the page binding carries the organisation, the viewer and the reach a page acts within - #1285
Merged
antoinetoussaint-byte merged 3 commits intoOct 30, 2026
Conversation
… and the reach a page acts within
A mounted page (solution or module) received apiBase, moduleApiBase and the
token accessors, but not what it needs to act for the viewer in their
organisation. SolutionBinding gains organizationId, viewerKey and
reach: Record<alias, {installationId}> beside the existing installationId.
The installation route fills them from the two reads the menu already
makes: the gateway-verified entitlements (org, viewer, each installation)
and the registry snapshot, which now carries each record's declared
consumption (`reaches`: the aliases of the modules its applied manifest's
invoke bindings name as audiences). A declared module joins to an
installation only through the viewer's own entitlement for it.
@codefly-dev/saas-ui and the co-versioned @codefly-dev/ui move to 0.13.5.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
Author
Review: module-saas-starter #1285 (1f6002c)BlockersNone. Security
Follow-ups (ranked)
Kit and tests
VERDICT: merge |
… which half of the rule it applies Review findings on the page-binding change: - A principal two aliases carry now resolves to no reach, instead of to whichever alias a map iteration met first, and the cache logs it when it appears or changes. - reachedModuleAliases names its source (accounts' readinessFor) and the half it cannot apply: the derived module registry's withholding lives in accounts alone, so a withheld binding is still listed and the module refuses its calls with a reason. - The /solutions/_registry route is tested to carry `reaches`. - viewerKey is documented as the effective viewer: the impersonated person's during an impersonation. - The reach test no longer names a module outside this repo's boundary. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ns again pageBinding takes the navigation's own join (entitledSolutions over the registry and the verified viewer's entitlements) from the route instead of making it internally, so the route reads as every projection must: through installations and the viewer's grants (solution_registration_boundary_test). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Oct 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
Every mounted remote — a solution page or a module page — receives
apiBase,moduleApiBase,getAccessToken(andinstallationIdfor its own installation), but not what a page needs to act for the viewer in their organisation. Two consumers hit this:organizationId,viewerKeyandreach.*).The fields (
SolutionBinding,@codefly-dev/saas-ui/solution)All optional (a minor of the host↔remote contract; an older host injects none):
installationId(existing)organizationIdorgthe gateway verified in that same entitlements answerviewerKeyviewerthe gateway verified in that same answer, the key the host already uses for entitlementsreach: Record<alias, { installationId }>Declared consumption. The registry answer the page already loads did not carry it (its manifest is id/nav/frontend/backend/group/dashboard/surfaces), and the host's only existing copy,
ListOrganizationInstallations.reached_modules, is org-admin only. So the gateway's existing internal registry snapshot (/solutions/_registry) gains one field per record,reaches: the aliases of the modules the applied manifest's invoke bindings name as audiences (not its own principal, not headless-only, and only audiences an applied record in the snapshot answers to). This is the declared half of accounts' reached-module rule (readinessFor): accounts also drops a binding its derived module registry withheld (grant.OperationAudiences, the manifest's ask narrowed by a module envelope). That registry lives only in accounts, and no read the gateway makes carries it, so a withheld binding is still listed here. The module's authority hop refuses those calls with its reason, and installation readiness reports the binding as omitted. A principal carried by two aliases resolves to no reach and is logged. No new endpoint and no new RPC. The frontend stamps it from the record (liketargetId, never from the remote's manifest), andGET /api/solutions/<id>/installation(the route the outlet already reads) now answers{installationId, healthy, organizationId, viewerKey, reach}through a purepageBinding()builder inprojections.ts.SolutionOutletpasses all four to the remote and keeps only well-formed values.Intents are left out: the host holds no chat-intent list today, so there is nothing to hand over without a new source.
Kit versions
@codefly-dev/saas-ui0.13.4 → 0.13.5 and the co-versioned@codefly-dev/ui0.13.4 → 0.13.5 (with the lockfile).@codefly-dev/saas-plugin-contractand@codefly-dev/saas-plugin-reactdo not carry the binding, so they do not change.Tests
TestARecordReachesTheModulesItDeclaresItInvokes(deduplicates; excludes its own principal, headless-only bindings, an audience no applied record answers to, and a principal two aliases share) andTestTheRegistrySnapshotRouteCarriesARecordsReach; fullgo test ./...andgo vetgreen.reachfrom its declared consumption, excluding a declared module the viewer is not entitled to and an entitled module it does not declare; a solution gets the same binding; the existing 404 and refusal cases still hold.SolutionOutletpassesorganizationId,viewerKeyandreachto the remote, drops a malformed reach entry, and hands nothing on refusal.tsc --noEmit, eslint on the changed files,vitestoversrc/solutions,src/app/api/solutionsandpackages/saas-ui(885 tests),publish-frontend-kit.test.mjs, the naming gate andscripts/check-boundaries.sh: all green.Not verified: a live run. In particular, I have not checked on a cell that the carried module grant from a solution grant appears in the viewer's
ListSolutionEntitlementsanswer. If it does not,reachcomes back empty for a solution whose team got module access only through the carried grant.Review
Reviewed at 1f6002c: VERDICT merge, no blocker. Findings fixed in 8cb0cf1:
viewerKeyunder impersonation is the impersonated person's key. This is documented in the binding's doc comment, the saas-ui README,SOLUTION_REGISTRATION.mdand the frontendAGENTS.md, with no behaviour change./solutions/_registrycarriesreaches. Removing the field fails it.The reach test also stopped naming a module outside this repo's boundary.
Classification
This is a fix where the behaviour belongs. The binding is the host↔remote contract; the consumption is the applied manifest's own declaration, which the gateway already parses; the installations come from the verified entitlements answer. Reused: the registry snapshot, the entitlements read, the
entitledSolutionsjoin, the installation route and the outlet's existing fetch. Deliberately not added: a new endpoint, a per-page RPC, any organisation or viewer value decoded from a token.Note for consumers: the directory page currently types
reach.recordsas a Connect base URL. This binding hands{ installationId }per alias; the page's base URL staysmoduleApiBase/ its composed endpoint.🤖 Generated with Claude Code