cd test/ui
npm install
npm test # matrix + uiGate + controls + forbidden suites
npm run preview # visual preview: http://127.0.0.1:8787/
node run.mjs --served # behavioural smoke against the deployed pageDeclarative UI gating
This guide is the contract for the declarative gating attributes read
by the injected guard script. The behavioural matrix at the end is the
one executed by test/ui/controls.mjs against the page fixture
test/ui/controls.html.
Attributes
| Attribute | Meaning |
|---|---|
| The action is neutralised while anonymous: |
| The native action proceeds while anonymous; the login tab opens as an invitation. The 401 request produced by the action is queued and replayed after login. Activation only: a deactivation does not open the login tab. |
| For value controls ( |
| Hidden while anonymous, shown once authenticated. Activating clicks are neutralised and show the sign-in banner; a checked checkbox may still be unchecked (deactivation). |
| Visible while anonymous, hidden once authenticated (pure affordance,
composable with |
| Identity-conditioned visibility, evaluated after the session probe. |
| Wires the click to the OIDC logout navigation. Authenticated only. |
Markup wins over the equivalent uiGate rule: an element carrying a
data-gtw-auth (or data-gtw-auth-hidden) attribute is not affected
by a config rule for the same dimension.
data-gtw-claims (and uiGate[].claims) takes a JSON object: each key
must match, each value is one accepted string or a list of accepted
strings (OR / anyOf on the same claim), and "*" means "claim present".
A token claim that is itself a list (groups, token roles…) matches
when one element matches. Example:
data-gtw-claims='{"email": ["alice@example.com", "a.rolland@example.com"]}'.
Control families
The guard distinguishes three kinds of interaction:
| Family | Controls | Rule |
|---|---|---|
Click is the action |
| Gated on click. The state read at click time decides activation vs deactivation. |
Change is the action |
| The click only opens the picker and is never gated. The |
Click is focus |
| Never gated on click: the click only focuses the field. Put
|
Notes:
resetis always a deactivation: it never opens a login, even withdata-gtw-auth. Use a normal button if the reset itself must be gated.fileis an activation: the picker is opened only after login. To allow choosing a file anonymously and gate the upload, put the attribute on the submit button.A radio’s pre-click state is remembered on
pointerdown/keydown; clicking the already-selected radio is a no-op and does not open the login. A synthesised click without those events is treated as an activation.detailsusesopenat click time: closed → opening is an activation; open → closing is a deactivation.aria-*toggles use the attribute value at click time:"false"→ activating;"true"→ deactivating. The application toggles the attribute in its own click handler (which the guard lets run only when the action is allowed).data-gtw-auth-offis read from the element, its gating ancestor or itslabel[for]; a value on the element overrides a value on the ancestor.
Behavioural test matrix
The matrix is executed by test/ui/controls.mjs. Each row runs in a
fresh jsdom window with the guard rendered from
internal/proxy/guard/guard.js.tmpl; login is a counted tab stub.
| Case | Interaction | State before | Expected |
|---|---|---|---|
Checkbox | click | unchecked | login, box stays unchecked |
Checkbox | click | checked | no login, box unchecked |
Checkbox | click on wrapping label | checked | no login, box unchecked |
Checkbox | click | unchecked | login, box checked |
Checkbox | click | checked | no login, box unchecked |
Checkbox, programmatic |
| unchecked | no login (no DOM event; documented limit) |
Radio | pointerdown + click on label | already selected | no login, stays selected |
Radio | pointerdown + click on label | not selected | login, stays unselected |
Radio | pointerdown + click on label | already selected | no login, stays selected |
Radio | pointerdown + click on label | not selected | login, stays unselected |
Select | click (open picker) | any | no login |
Select | change to a non-off value | off | login, value reverted; app change handler not run |
Select | change to the off value | non-off | no login; app change handler runs |
Select | click (open picker) | any | no login |
Select | change to a non-off value | off | login, value kept; app change handler runs |
Select | change to the off value | non-off | no login; app change handler runs |
Select, default off | change away from the default | default | login, reverted to the default |
Select, default off | change back to the default | non-default | no login |
Select, explicit off | change to | other | no login, value kept |
Select, explicit off | change away from it | off | login, reverted to the off value |
Select, attribute on | click on the label | any | no login (click is passive) |
Select, attribute on | change to a non-off value | off | login, value reverted |
Select, attribute on a wrapper | click on the select | any | no login |
Select, attribute on a wrapper | change to a non-off value | off | login, value reverted |
Range | click | any | no login |
Range | change to a non-off value | off | login, value reverted; app change handler not run |
Range | change to the off value | non-off | no login; app change handler runs |
Range, default off | change away from the default | default | login, reverted to the default |
Range, default off | change back to the default | non-default | no login |
Date | click | any | no login |
Date | change to a non-off value | off | login, value reverted |
Date | change to the off value | non-off | no login |
Text / textarea / number | click | any | no login; native click preserved |
File | click | any | login; native click neutralised |
Button | click | any | login; native click neutralised |
Submit | click | any | login; native click neutralised |
Reset | click | any | no login; native click preserved |
Details | click on summary | closed | login, stays closed |
Details | click on summary | open | no login, closes |
Details | click on summary | closed | login, opens |
Details | click on summary | open | no login, closes |
| click |
| login, stays |
| click |
| no login, becomes |
| click |
| login, becomes |
| click |
| no login, becomes |
| click |
| login, stays |
| click |
| no login, becomes |
| click |
| login, stays |
| click |
| no login, becomes |
Hidden checkbox, via label | click | checked | no login, unchecked; no banner |
Hidden checkbox, via label | click | unchecked | sign-in banner, stays unchecked; no tab |
Checkbox, | click on label | checked | no login, unchecked |
Checkbox, | click on label | unchecked | login, stays unchecked |
Checkbox, wrapper | click | checked | no login, unchecked |
Checkbox, wrapper | click | unchecked | login, stays unchecked |
| click | any | no login |
| change to a non-off value | off | login, value reverted |
| change to the off value | non-off | no login |
| click | any | no login |
| click | any | no login |
| click | any | login |
uiGate equivalence
A config rule {"selector": "…", "auth": "before"|"after"} has the
same semantics as data-gtw-auth, including the activation-only rule,
the passive-control exemption and the change-based gating of value
controls. For a value control the OFF value is the control’s default
value; a data-gtw-auth-off attribute on the element overrides it.
A config rule {"selector": "…", "hidden": "before"} keeps the
hidden dimension: it neutralises activating clicks and shows the
banner, but a deactivation of a checked checkbox is still free.
Limitations
Single-select only:
select multipleis not covered by the value-off comparison in this version.inputevents are not gated: forrangeand date/time/color pickers, the guard reacts tochange. An application that updates live oninputsees the transient value before abeforerevert.Programmatic checkbox/radio changes are invisible: setting
el.checked = truefires no DOM event. The login is requested at the first real request (401 → banner) or explicitly by the application throughwindow.__cladoneAuth.login().Radio arrow-key selection is not click-gated: moving the selection with the arrow keys fires
changewithoutclick. Clicking (or Space) is gated; arrow-key selection is left to the server-sideauth.whenpolicy. A dispatchedchangeon a gated radio is also not reverted by the click-based state machine.Ambiguous wrappers: with several controls under one gated wrapper, put
data-gtw-authon each control. The guard resolves the target’s own control when it can, otherwise the first match.resetis always free: use a normal button to gate a reset.fileclick is an activation: gate the submit button if the picker itself must stay available anonymously.
Running the tests
The fixture page is test/ui/controls.html; the assertions are in
test/ui/controls.mjs. The guard script is rendered from the source at
every run, so a stale embedded script can never pass the suite.
Local visual preview
npm run preview starts a small local server that injects the guard
into controls.html and provides a demo auth-bridge:
http://127.0.0.1:8787/— the controls page with the guard injected; each case is preceded by a label naming the element, thedata-gtw-*attribute and the value exercised;a gated control opens a login tab with a Sign in (demo) button; the demo session is stored in a cookie and the tab posts
cladone-auth:okback to the page;Force authenticated, Reset demo session, and the user-card logout change the demo session in place: no redirect, no reload, so the page keeps its scroll position and the focused feature;
the user-card logout is a preview override of
window.__cladoneAuth.logout(); production still performs the OIDCend_sessionnavigation;/?authed=1and/logoutremain available as manual shortcuts;the toolbar shows the current state;
the badge is at the top right: approach the top edge (the toolbar reserves that corner and stays below the badge) to reveal it, then click it to unfold the claims and the logout button.
The preview is for visual inspection; the assertions remain in
controls.mjs and are run by npm test.