Test coverage
This page renders the file the Playwright integration suite executes, e2e/scenarios.json, as it is. Every scenario runs as each of the profiles below, and the suite checks BOTH that the allowed profiles get through AND that every other one is refused - an access test that only proves the yes half proves nothing.
The profiles
| Profile | What this account is |
|---|---|
root |
Root: full control of the gateway |
infra-admin |
Gateway admin: routes and built-in pages only |
app-admin |
Application admin: users, roles and identity settings only |
tenant-admin |
Tenant admin: ADMIN member of the acme tenant |
user |
Plain user: USER member of the acme tenant, no capability |
Gateway scope
| Scenario | Probe | Allowed |
|---|---|---|
| List the routes The routing plane belongs to the gateway scope: only root and infra-admin may read the route table; every other profile gets 403. | GET /api/routes |
root infra-admin |
| Deposit an OpenAPI spec on a route A route may hold its contract as a deposited file rather than an upstream url, and the gateway then serves it on the route's own prefix. Depositing belongs to the routing plane: root and infra-admin only, every other profile gets 403. The probe names a route that does not exist, so an allowed caller gets 404 - which proves the barrier without depending on a fixture route. | PUT /api/routes/no-such-route/spec?filename=orders.yaml |
root infra-admin |
| Choose which operations of a route are audited An audited operation's calls become audit events sent to the OpenTelemetry collector, with the caller, the status, the trace id and the fields the operation names. Choosing them belongs to the routing plane: root and infra-admin only, every other profile gets 403. The probe names a route that does not exist, so an allowed caller gets 404. | PUT /api/routes/no-such-route/audit |
root infra-admin |
| Remove a route's deposited spec Removing drops the file AND the declaration that names it, in one move: a route left pointing at a file that is gone is the dangling reference this feature exists to avoid. Same plane and same holders as the deposit. | DELETE /api/routes/no-such-route/spec |
root infra-admin |
| Preview a version predicate The editor asks the ENGINE what a version pattern extracts while it is being typed, rather than guessing in the browser: Go uses RE2, which refuses lookarounds JavaScript allows, so a preview computed there would promise a match the gateway turns down at save. It reads and writes nothing, but it belongs to the routing plane and follows the route table's scope. | POST /api/routes/version-preview |
root infra-admin |
| Read the brick catalogue The catalogue of predicates and filters drives the route editor: gateway scope. | GET /api/catalog |
root infra-admin |
| List the external authorities A directory or an identity provider is a third-party service reached by URL, with credentials: infra plane. An app admin configures who may do what, not what the gateway connects to. | GET /api/auth-providers |
root infra-admin |
| Probe where a request lands Runs a request through the live matcher and reports every route's verdict, in order. Routing is the infra plane's: an app admin cannot inspect it. | POST /api/routes/probe |
root infra-admin |
| Export the vault, encrypted Seals the entries the caller administers under a passphrase, and only those: an app admin's export holds no infrastructure entry, a tenant admin's holds only their organisation's. This is the half a configuration export never carries, which is what lets a configuration be public. A profile administering no vault at all is refused; 422 means the caller's scopes hold nothing yet. | POST /api/vault/export |
root infra-admin app-admin tenant-admin |
| Export the whole configuration Downloads routes, roles, authorities, mail relay, themes and settings as one YAML file. Root only: a configuration crosses both planes at once, so administering one of them is not enough. Secrets never travel - a declared secret field leaves as its $name reference or not at all, which is what makes the file safe to put in a ticket or a git repository. | GET /api/config/export |
root |
| What an export will not contain Lists the secret fields holding a literal rather than a vault reference: they stay behind, so the console can say so next to the download button, while the admin can still act on it. Lists as well every $name the configuration expects, which is what the target gateway will have to hold in its own vault. | GET /api/config/report |
root |
| Preview what importing a file would do Reports what an imported file would add, change or remove, and which vault entries it expects that the vault does not hold, without writing anything. That preview is the one moment an admin still knows what a given secret was, which is why the missing ones are asked for right there. | POST /api/config/preview |
root |
| List the saved configurations Several configurations coexist and one is active (CFG-02). Root only, like export and import and for the same reason: a configuration crosses both planes at once, so whoever handles one administers both. Reading stays open on the community image - what the Enterprise one adds is KEEPING several - so an installation coming down from it still sees what it saved. | GET /api/configurations |
root |
| List the git locations The named git locations a configuration travels through (CFG-07): a repository, a branch and a directory each, so the same repository can hold one directory per customer platform. Root only, like every configuration - a location names a repository and a vault reference for its token, which is the installation's own business. | GET /api/config-remotes |
root |
| Check a git location without writing to it Proves the repository answers, the credential is accepted and the branch exists, and writes nothing - so a wrong token is found on the screen that sets it rather than during the first push of a real change. A refused credential comes back with what that forge actually wants, because every forge reports a missing permission and a wrong basic-auth username as the same sentence. The probe names a location that does not exist, so a 404 proves the barrier without a fixture. | POST /api/config-remotes/no-such/check |
root |
| Pull a configuration from git, applying nothing Reads a configuration out of its git location and SHELVES it: the document lands as a saved copy and the gateway goes on serving what it serves. The answer carries the plan - what activating it would add, change and remove - because the loop is pull, read, activate, read again. Whoever can write to the branch must not be able to reconfigure the gateway, which is why nothing here applies. | POST /api/configurations/no-such/pull |
root |
| Push a saved configuration to git Commits the SAVED copy to its git location, attributed to the operator who clicked - which is what links the audit trail to the repository's history. A push replaces the location's directory, whatever is there, and never rewrites history: what it replaced stays in the repository's log. The one refusal left is somebody else pushing at the very same moment, and the cure is to push again. | POST /api/configurations/no-such/push |
root |
| Send a mail relay test message The mail relay is infrastructure (a third-party host with credentials): only infra admins may fire the test (422 while it is not configured). | POST /api/settings/mail-relay/test |
root infra-admin |
| Read where the database lives and what the gateway runs on The Database tab prints its backup, restore and migration procedures from this answer: the real paths, the platform the runtime describes (Kubernetes and its Helm release, a Swarm service, a compose project) and the copy a migration is in the middle of, never its password. Root only: it names the installation's insides. | GET /api/backup/info |
root |
| Dismiss the remembered copy The gateway remembers the last copy into a PostgreSQL server, so the migration procedure survives a reload; an operator who went another way dismisses it. Root only, like the copy itself. | DELETE /api/backup/last-copy |
root |
| List control-plane access tokens Control-plane tokens grant headless access to the admin port: this is what connects an agent (MCP-01). Anybody who administers a domain (root, infra-admin, app-admin) mints and lists their OWN: a token acts with its owner's capabilities, read again on every call, so it never hands out more than the person holds. The tokens authenticate on the admin plane only, never the data plane, and each carries a perimeter (readonly or full). | GET /api/admin-tokens |
root infra-admin app-admin |
| The agent endpoint's switch The MCP endpoint ships OFF (MCP-01) and only root or an infra admin may open it: a control-plane token is required either way, but the door's existence is a decision somebody takes, not something inherited from an upgrade. | GET /api/settings/agent |
root infra-admin |
| Change what a token may do A token is editable for everything that is NOT in the token. The secret is a hash in a column and encodes no perimeter, no domain, no address, no expiry and no name, so none of them has to be reissued to be changed - and whoever holds the key keeps holding the same key. Which is what makes NARROWING cheap, and that is the reason this exists: a read-only token in a service's deployment manifest should become a schedules one without minting a second and editing another team's repository, because that friction is why the safer move does not get made. Widening is possible too, and the audit records the before and after of every field. Open to whoever may mint one - root, infra-admin, app-admin - on their own tokens only: another person's answers not found. | PUT /api/admin-tokens/no-such-token |
root infra-admin app-admin |
| Give a token a new secret A rotation, and what it KEEPS is why it is not simply minting a second one: the name, the perimeter, the domain, the addresses and the expiry all stay, so the audit keeps one history for one credential and the only thing to change anywhere is the value in one configuration file. The old secret stops working the moment this answers - which is what a rotation is, and why the console asks first. The new value travels exactly once, in this response, like a freshly minted one. Whoever may mint one, on their own tokens only. | POST /api/admin-tokens/no-such-token/renew |
root infra-admin app-admin |
| The developer tunnel (plug) Whether the developer tunnel is open (DEV-11), the address developers type to reach it, what it is doing on this node, and who holds the developer capability with or without a deposited key - by fingerprint, never the key. Infrastructure, beside developer mode rather than under it: developer mode is a configuration of what the installation offers its developers, this opens a port into the cluster and uses the deployment's rights over it. The tunnel runs only when both say so, ships OFF, and the page where a developer deposits a key exists only while it is open. | GET /api/settings/plug |
root infra-admin |
| Where this gateway's traces go The collector this gateway exports its spans to (OBS-04), and whether this image can export at all. Whoever edits routes - this is infrastructure, like TLS or the mail relay - and Enterprise to turn on: the trace context TRAVELS in both editions, and an identifier is put on every request whether or not anybody is tracing, because that identifier is what joins a line of the access log to a service's own business audit. What is sold is putting Meerkat on the trace: its inbound span, the span of the upstream call, and the gap between the two, which is the gateway's own time and the one number nobody else in the cluster can produce. What comes back is RAW: a header value of $otlp-token is the reference as written, never the secret behind it - the exporter resolves it at the moment of the call, so the credential is not in this answer, not in the audit trail and not on the screen. | GET /api/settings/telemetry |
root infra-admin |
| Ask the collector whether it answers Posts an EMPTY OTLP batch to the address on screen, before anybody saves it: the whole path is exercised - name resolution, network, TLS, the credential, the collector's own answer - and no span is recorded anywhere. Gateway scope, like the setting it tests, because it makes this gateway call an address somebody hands it; the answer carries the status and the time it took, never the body. | POST /api/settings/telemetry/test |
root infra-admin |
| Approving an agent is a root decision The consent page (MCP-07) hands an agent the approver's own powers over the whole gateway, so only root reaches it; everyone else is refused before seeing it. Root reaches the handler and 400s on the probe's unknown client, without registering anything. | GET /oauth/authorize?response_type=code&client_id=none&code_challenge=x&code_challenge_method=S256 |
root |
| Drive the gateway from an agent One endpoint, the Model Context Protocol over Streamable HTTP. It sits behind the same door as the API, and the tools it offers are the caller's own: a signed-in session reaches it, the tools then answer with that person's powers. Off by default, hence the 404 on a fresh installation. | POST /mcp |
root infra-admin app-admin tenant-admin user |
| The console documents the control plane Every signed-in console user may open the swagger-ui page: it lists Meerkat's own admin API, and nothing else - the routes' specs live on the data plane, for developers. Anonymous callers get 401. | GET /apidocs/specs.json |
root infra-admin app-admin tenant-admin user |
| A service schedules a call to itself, and the gateway makes it Scheduled calls (SCHED-01): root mints a schedules-perimeter token, a backend creates a schedule on the control plane - saying which ROLES its call must carry, in the singular or the plural - finds it again by its own metadata, and the gateway calls the named route through its front door as "meerkat" holding them; the run closes with what answered. No account behind the call, and the token opens nothing else on that port. The history (SCHED-03) keeps every turn that ended, and a turn is asked for again by name - a new run carrying today's payload, which says in the history which one it replays. | _flow_ | user |
| List the TLS certificates What the gateway presents at a handshake is infrastructure, like the upstream of a route: only infra admins may read the certificates. Key material never appears in the payload, whoever asks. | GET /api/certificates |
root infra-admin |
| Generate a self-signed certificate HTTPS in one click, for a laboratory or a first integration: the gateway is its own authority, and the certificate says so. | POST /api/certificates/self-signed |
root infra-admin |
| Create a certificate signing request The offline door: the request leaves for whoever runs the authority, the private key stays. Nothing here needs a route to the internet. | POST /api/certificates/signing-request |
root infra-admin |
| Ask the ACME authority for a certificate An order is added from a plane's Add certificate menu like the others and sits on that plane; the authority is asked on the first visit to a name once the order serves a door. Refused while no authority is set up, and for a wildcard or an address. | POST /api/certificates/acme |
root infra-admin |
| List the ACME authorities The accounts certificates are asked of, and the providers the drawer offers. A literal account key never travels back. | GET /api/acme/authorities |
root infra-admin |
| Set up an ACME authority Refused at save time rather than at renewal time: terms not accepted, a binding missing, a directory that does not answer - each named. | POST /api/acme/authorities |
root infra-admin |
| Change an ACME authority Another directory is another account: it gets a fresh corner of the cache. On the community image the edition refuses first (422). | PUT /api/acme/authorities/no-such |
root infra-admin |
| Delete an ACME authority Refused while certificates are still asked of it, and the refusal names them. | DELETE /api/acme/authorities/no-such |
root infra-admin |
| Ask an authority again Once whatever the authority refused for is fixed - a DNS record, a closed port - the order is asked again. On the community image the edition refuses first (422). | POST /api/certificates/no-such/retry |
root infra-admin |
| Place a certificate on a plane A certificate is made or imported once, then served on the console, the applications, or both. Another certificate already answering for one of its names on that door is named in the refusal, and replaced only when asked. | PUT /api/certificates/no-such/placement |
root infra-admin |
| Read the TLS settings Which HTTPS doors are open, and whether an ACME authority issues on its own. The external-account HMAC key follows the vault rule: a literal never travels back. | GET /api/settings/tls |
root infra-admin |
| Compare two saved configurations What changes going from one saved configuration to another, object by object, without looking at what runs (CFG-04): the question asked of a customer's configuration and the template it was made from, before touching either. Root only, like every configuration. | GET /api/configurations/no-such/compare/other |
root |
Application scope
| Scenario | Probe | Allowed |
|---|---|---|
| Manage the built-in pages' themes The flow pages (login, profile) carry the PRODUCT's face - name, logo, colours - so their themes and branding are application scope. The gateway merely serves them. | GET /api/themes |
root app-admin |
| Correct and add the built-in pages' languages The wordings of the sign-in page are the PRODUCT speaking, so they sit in application scope beside its theme and its brand. The same right covers correcting a shipped catalogue and adding a language the binary does not carry. | GET /api/locales |
root app-admin |
| List the users The user base is the application's identity: app-admin scope. A gateway admin has no business reading it. | GET /api/users |
root app-admin |
| Who may plug in Who holds the developer capability, with the keys they deposited by fingerprint, and whether the tunnel is open (DEV-13). The people of the tunnel are the application's: its administrator grants the capability, so it reads this; the tunnel's switch stays the infrastructure's. | GET /api/plug/developers |
root app-admin |
| Who is plugged in now The plug runs open now, merged over the gateway's replicas (DEV-13), read by the administrator who can end one: the application's, who grants the developer capability. | GET /api/plug/sessions |
root app-admin |
| Read the role catalogue The global hierarchical role catalogue (RBAC-01) is application scope. | GET /api/roles |
root app-admin |
| List every account's application tokens The tokens each person created on their profile to call the applications' APIs (AUTH-09), with their owner: what an application administrator needs to find the one a departed colleague's script still carries, or the one that leaked. | GET /api/data-tokens |
root app-admin |
| Revoke an account's application token Ends it at its next call, and writes token.revoke to the owner's line of the trail. An administrator ends a token here, never mints one: a credential is only created by its owner. | DELETE /api/data-tokens/no-such-token |
root app-admin |
| List the live sessions Who is signed in where, right now (CONSOLE-08), with filters and pages. Root reads both planes; an application administrator the applications' sessions only, since who runs the console is root's business. | GET /api/sessions |
root app-admin tenant-admin |
| End a session Signs one session out on every gateway (SEC-07), and writes session.revoke to the trail. A console session is root's alone: under an application administrator's eyes it reads as absent. | DELETE /api/sessions/0000000000000000 |
root app-admin tenant-admin |
| Read the audit trail The audit trail is a transverse section scoped by capability (RBAC-05): root sees all, infra-admin the routing plane (routes, themes), app-admin the identity (users, roles, settings), a tenant admin only their tenants' events. Anyone who administers nothing is refused. | GET /api/audit |
root infra-admin app-admin tenant-admin |
| Export the audit trail The trail as a CSV file (STORE-06): the filters of the screen and the perimeter of the caller, never more than the screen shows. Enterprise, and audited - the file carries addresses and names, and who took it out is a question somebody will ask. | GET /api/audit/export?kind=security |
root infra-admin app-admin tenant-admin |
| How long the audit trail keeps an event The retention of the trail (AUD-02), from three months to five years. Root's alone: whoever may shorten the trail may erase their own traces with it. | GET /api/settings/audit |
root |
| Read the issue reports The embedded issue tracker (ISSUE-03) is a transverse section: root and infra/app admins see every report, a tenant admin only those filed under their tenants. Anyone who administers nothing is refused. | GET /api/issues |
root infra-admin app-admin tenant-admin |
| An app admin can never mint or promote a root Creating a root user or setting the root flag on an existing user requires root itself: the API answers 403 to an app-admin. | _flow_ | - |
Infrastructure scope
| Scenario | Probe | Allowed |
|---|---|---|
| Read the maintenance switch The switch that closes an installation to its visitors (LIFE-05) answers 503 for every route at once, without a single route being edited. Why it is closed is picked from a translated list rather than typed, because that page is read in twenty languages. Closing every application is an operator's act: only root and infra admins read or flip it, and whoever administers or develops can still go through to check the fix before reopening. | GET /api/settings/maintenance |
root infra-admin |
| List what the runtime can route to What the runtime says exists beside this gateway (SVC-02), asked when a route is created: an upstream typed by hand is where the typos live, and none of them show up until traffic does. Always available in both editions and with no switch - what gates it is what the deployment grants, a mounted socket or a service account. An installation with no runtime to ask answers WHY rather than failing. | GET /api/services |
root infra-admin |
| Subscribe to what a screen shows One websocket for the whole console, with a source per screen: the traffic curves, the scheduled calls, and the journal of administrative writes every screen watches to learn that what it is showing moved. It answers every ADMINISTRATOR - the routing plane's, the application's, an organisation's - and WHAT each may watch is decided at the upgrade, from the audit trail's own partition, then handed to the socket as a registry holding those sources and nothing else. A source cannot check it: the library shares one read between the subscribers of a topic. It is mounted BEHIND the same funnel as every API call, so a subscription passes the same checks: session, pending login, token perimeter, narrowing to the token's domain. A plain GET is not an upgrade, so an allowed caller is refused by the protocol rather than by the perimeter, which is the point of the probe. | GET /api/live |
root infra-admin app-admin tenant-admin |
| Read what passed through Traffic per route over the last hour: requests by status class, latency, upstream failures (OBS-01). It tells a route that is failing from one nobody is calling - the two look identical in a configuration. Summed over every node of a cluster, and each interval says how many it covers, so a partial curve never passes for a total. In memory: a restart starts a new hour, and an installation wanting a year pushes the counters over OTLP (Enterprise) to a collector that writes them into the time-series database it already runs. With ?endpoints=1 it also answers WHICH endpoint of a route the time went to, over the period ?since asks for - the same one the caller is drawing, so a route's endpoints add up to the route rather than to a longer story nobody asked about. The unit is the template, never a raw path - one series per order is chosen by whoever sends the requests, which makes it a denial of service and not an imprecision. A name is either DECLARED (a deposited spec, the per-endpoint rules, or what the control plane resolved from the upstream) or DEDUCED from the shape of the paths seen, with identifier-looking segments folded to {id} and a per-route budget past which everything shares one bucket. Deduced lines are marked, because a shape can be read wrong. | GET /api/metrics |
root infra-admin |
| Read which routes are answering The console has always shown what is CONFIGURED and never what WORKS (SVC-04). This reports what the circuit breaker already watches on every answer, per node - so it needs no prober of its own, and it sits on the routing plane's perimeter like the routes it describes. | GET /api/routes/health |
root infra-admin |
| Read the gateway's own log The last lines this node wrote about itself (OBS-03), structured whatever the output format, for the console's Logs screen. Per node: in a cluster the answer names the node, and the whole cluster's log is the collector's business. The access log is not in it. | GET /api/logs?limit=1 |
root infra-admin |
| Turn the log level up or down Live, on every node, and not stored: a restart comes back at the manifest's level. A level more talkative than the startup one goes back on its own after thirty minutes, because a gateway left at debug fills a disk. The probe sends a level that does not exist, refused after the guard, so the run never changes what the gateway writes. | PUT /api/logs/level |
root infra-admin |
| Read the gateway's own limits How much memory the proxy may spend on a body it rewrites (PERF-02) is a property of the INSTALLATION, not of the application it serves - so it lives in the Routes page's Global drawer, with the other things that are true of every route at once, and an application administrator cannot move it. | GET /api/settings/proxy |
root infra-admin |
| Read the issue tracker switch Turning user-filed issue reports on is an infrastructure decision (ISSUE-04): the switch sits on the Issues screen, ships OFF, and only infra admins read or flip it. | GET /api/settings/issues |
root infra-admin |
Console navigation
| Scenario | Probe | Allowed |
|---|---|---|
| Read what the running release brought The version under the account button, and the notes of the minor or major release it belongs to: 1.0.0 for 1.0.1, never a patch's. Any signed-in account reads it. | GET /api/release-notes |
root infra-admin app-admin tenant-admin user |
| The Infra rail entry shows only for its admins The console rail hides the Gateway scope from profiles that cannot use it (and the API refuses them anyway). | _ui_ | root infra-admin |
| The Application rail entry shows only for its admins Users, roles and identity settings are gated behind the app-admin capability in the console navigation. | _ui_ | root app-admin |
| The Tenants rail entry shows for tenant admins A tenant owner or ADMIN (or a tenant creator) sees the Tenants scope; plain members and capability admins without a tenant do not. | _ui_ | root tenant-admin |
| The Data plane rail entry shows for administrators What the applications are doing - their sign-ins, sessions, scheduled calls, traffic and issue reports - in one entry with a section each. It shows for anyone who may open one of those sections; each section is guarded on its own and the API scopes the content. | _ui_ | root infra-admin app-admin tenant-admin |
| The Meerkat rail entry shows for everybody holding the console The gateway itself - its audit trail without the applications' sign-ins, its own log, the console's sessions, the API reference, the release notes and the licence. Everybody reads the last two, so the entry shows for every profile; each other section shows only for who may open it. | _ui_ | root infra-admin app-admin tenant-admin user |
| Each profile lands on its own first screen Opening the console root: infra admins land on /infra/routes, app admins on /application/general, everyone else on their tenant. | _flow_ | root infra-admin app-admin tenant-admin user |
| Single-organisation mode is switched live, and gives the others back The shape of an installation is a setting read per request, not a value frozen at boot: switching it takes effect at once. Going down to one organisation holds the others back without deleting anything - the console is served knowing it, on the first paint - and switching up gives them and their people's access straight back. | _flow_ | root |
Tenant scope
| Scenario | Probe | Allowed |
|---|---|---|
| List a tenant's members Tenant administration is ownership or the ADMIN membership on THAT tenant: neither capability admins nor plain members reach another tenant's member list. | GET /api/tenants/{tenantId}/members |
root tenant-admin |
| Read a member's sign-in history The sign-in history of a member is visible to root and to that tenant's owner or ADMIN, nobody else. | GET /api/tenants/{tenantId}/members/{aliceId}/logins |
root tenant-admin |
| Transfer tenant ownership Ownership is a tenant field (TENANT-02): only root or the current owner may transfer it. A tenant ADMIN reaches the endpoint but is refused (403); root reaches the handler and 422s on the probe's unknown user, without changing anything. | POST /api/tenants/{tenantId}/owner |
root |
| Exclusive group mode asks which group at sign-in With a tenant forced to SINGLE, a member holding several groups lands on /select-group after signing in (and again after each tenant switch); only the chosen group's roles apply to the session. | _flow_ | user |
Application screens
| Scenario | Probe | Allowed |
|---|---|---|
| Choose between one organisation and several The shape of the identity - who belongs where - so it is the app-admin's decision, not root's alone. The probe sends a mode that does not exist, refused after the guard, so the run never switches the installation it is testing. | PUT /api/settings/tenancy |
root app-admin |
Sign-in and profile flows
| Scenario | Probe | Allowed |
|---|---|---|
| A wrong password is refused without a session The login page answers 401 with the same message whether the user or the password is wrong (no account enumeration) and no session cookie is issued. | _flow_ | - |
| A user sees their own sign-in history After signing in, /profile/security links to /profile/history where the fresh sign-in shows with its method. | _flow_ | user |
| The profile flow links back to the applications The profile hub (and the user button's Applications submenu) lists the UI routes the session may open - public, authenticated, and granted role-gated ones only. | _flow_ | user |
| The developer page refuses non-dev users /profile/dev answers 403 for a user without the dev capability. | _flow_ | - |
| The built-in captcha gates self-registration With the home-grown captcha on (the default), the register page carries a distorted-digits image; a wrong copy is refused, creates no account, and each challenge is single-use. | _flow_ | - |
| Self-registration end to end With the policy on and SMTP configured: the visitor registers, cannot sign in before confirming, opens the mailed one-shot link, the admins are notified, and the first sign-in lands on the waiting room until access is granted. | _flow_ | - |
| Personal API tokens authenticate API calls A user mints a token on Profile > Security > API tokens (shown once); it captures their current tenant+group context and authenticates 'Authorization: Bearer mk_...' calls to the data-plane API routes with no browser session. Disabling or revoking it - or disabling the account - stops it at the next request; the admin port never accepts it. | _flow_ | user |
| Brute-force sign-ins are throttled Failed sign-ins are counted per address and account within a configurable window (Security page): past the budget the endpoint answers 429 without even checking, and a success forgives the counter. Failures on one account from any address are counted too: past five the page asks for the picture code as well, an unknown name exactly like a real one, and never locks anything. Wrong two-factor codes are throttled per account the same way. | _flow_ | - |
| Forgot password resets through a mailed one-shot link The sign-in page links the flow (when SMTP is configured): a neutral outcome whatever the address, a 1-hour one-shot link that survives mail-scanner prefetches (GET peeks, POST consumes), and a successful reset revokes every open session and notifies the owner. | _flow_ | - |
| Disabling passkeys removes the affordance and refuses the ceremonies When the global passkey policy is off, the login page hides the passkey button and the WebAuthn endpoints answer 403. | _flow_ | - |
| An account awaiting access passes "signed in" and stops at "in an organisation" The belonging axis (RBAC-06) over a real gateway: a confirmed account that belongs to no organisation still passes a route gated on authentication - that level says who it is and nothing more - but a route asking for an organisation sends it to /account-pending rather than answering a bare 403. | _flow_ | - |
Data plane
| Scenario | Probe | Allowed |
|---|---|---|
| A mailed code signs in, and only in the browser that asked Sign-in by one-time code (AUTH-16), switched on for the run: the sign-in page offers it, the code arrives through the SMTP sink, it opens the session - and the same code pasted into a second browser is refused, which is what the request-id cookie is for. | _flow_ | user |