feat: give display devices their own paired identity
ADR 0033 gave Ekklesia a display host and ADR 0034 gave it a login page. Together, the only way to light a television in a foyer was to sign a human being into it and leave that session running on a screen anyone can walk up to: a person's credentials on a wall, a person's session as the thing signage depends on, and the display going dark when they are revoked or their session simply expires. A screen is not a person. Ekklesia now recognizes three principal classes — anonymous, authenticated human, and managed device — and a device is a first-class principal with its own credential and its own explicit permissions. It has no email, no password, and no row in the identity tables, and it satisfies the `authenticated` baseline never, so every route that settles for "somebody is signed in" stays closed to it. Device permissions live in their own table because ekklesia_core_grant.user_id is a non-null foreign key into the user table and making it polymorphic would rewrite the platform's central authorization record to serve one narrow case. That is a schema decision, not a second decision point: the evaluator gained one branch, so a device is allowed or denied by the same function, with the same deny-by-default, as anyone else — with no owner path it could reach and no baseline that stands in for a grant. The six-digit code is not the credential. It is printed across a screen in a public hallway, so it is treated as public: it identifies a request to an administrator and is worth nothing alone. Collecting the credential also requires a 256-bit claim secret held only in the requesting browser's HttpOnly cookie, and approving requires the restricted core.devices.manage permission. Without that split, reading a code off a wall would hand over the screen's identity. The permission a pairing asks for is read from the module's validated display declaration, never from the request, so a browser cannot ask to be granted core.devices.manage or another module's permission. Approval grants exactly that one permission. Credentials are stored only as SHA-256; the raw value exists in the Set-Cookie that issues it and the device's own cookie jar, and is never logged, returned in JSON, or placed in a URL. The cookie is HttpOnly, so the module display code running in that same page cannot read it. Revocation is a server-side column checked on every principal resolution, and re-pairing mints a new identity so an old credential can never come back. State transitions are conditional updates, so one code cannot be approved twice or claimed into two credentials under a race. /api/v1/principal still reports a device as anonymous: it is what the Dashboard uses to decide whether a person is signed in. Displays read their viewer from the descriptor instead, which already performed the authorization — so displays are descriptor-first while the Dashboard stays principal-first, and neither infers an identity by elimination. Proven end to end against real PostgreSQL and real HTTP: an unpaired screen shows a code, an owner approves it, the device opens the display descriptor and a realtime stream, and revocation closes both while the device still holds its cookie. An already-open SSE stream is bounded by realtime's existing five-minute reconnect rather than closed instantly, and that bound is documented rather than called immediate. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3ce4fe03fe
commit
80189c8ae2
@@ -0,0 +1,192 @@
|
||||
# 0036: A display device is its own principal, paired by an administrator
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-08-24
|
||||
- Extends: [0013](0013-core-authorization-and-owner-bootstrap.md),
|
||||
[0032](0032-native-module-realtime-transport.md),
|
||||
[0033](0033-display-kiosk-host-contract.md),
|
||||
[0034](0034-host-owned-browser-authentication.md),
|
||||
[0035](0035-initial-platform-owner-bootstrap.md)
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0033](0033-display-kiosk-host-contract.md) gave Ekklesia a chrome-free display host, and
|
||||
[ADR 0034](0034-host-owned-browser-authentication.md) gave it a login page. Put together, the only
|
||||
way to light up a television in a foyer was to sign a human being into it and leave that session
|
||||
running on a screen anyone can walk up to.
|
||||
|
||||
That is not a display model. It means a person's credentials live on a wall, a person's session is
|
||||
what a screen depends on, and revoking that person — or their session simply expiring — takes the
|
||||
signage down. The alternatives are worse: a public unauthenticated display, a hardcoded token, or a
|
||||
credential in a URL.
|
||||
|
||||
A screen is not a person, and pretending otherwise is the problem.
|
||||
|
||||
## Decision
|
||||
|
||||
Ekklesia recognizes **three** principal classes: anonymous, authenticated human, and managed device.
|
||||
A device is a first-class principal with its own identity, its own credential, and its own explicit
|
||||
permissions.
|
||||
|
||||
```text
|
||||
screen opens /display/announcements → no identity → shows a short code
|
||||
administrator types the code in the Dashboard, names the screen, approves
|
||||
screen collects a credential → device principal → display and realtime work
|
||||
administrator revokes → the screen loses access and shows a code again
|
||||
```
|
||||
|
||||
### Devices are not users
|
||||
|
||||
There is no device row in the identity tables and no foreign key from a device to a user. A device
|
||||
has no email, no password, and no human session.
|
||||
|
||||
This matters beyond tidiness. Every "is a person signed in?" question in the platform is answered by
|
||||
`isAuthenticatedPrincipal`, and a device that satisfied it would inherit whatever a signed-in human
|
||||
may do. So a device satisfies the `authenticated` baseline **never** — not with one permission, not
|
||||
with ten. The only thing that makes a device allowed is an explicit permission granted to that exact
|
||||
device.
|
||||
|
||||
### Device permissions are their own table, and the evaluator is still the only judge
|
||||
|
||||
`ekklesia_core_grant.user_id` is a non-null foreign key into the user table. Generalizing it into a
|
||||
polymorphic subject would rewrite the platform's central authorization record — its
|
||||
relationship-derived grants, its audit semantics, its indexes — to serve one narrow case. Device
|
||||
permissions therefore live in `ekklesia_core_device_permission`.
|
||||
|
||||
That is a schema decision, **not** a second decision point. `createAuthorizationEvaluator` gained one
|
||||
branch, so a device is allowed or denied by the same function, with the same deny-by-default, as
|
||||
anyone else. There is no `if (device) bypass`, no owner check a device could reach — a device is
|
||||
never a platform owner — and no baseline that stands in for a grant. A deployment that composed no
|
||||
device support at all simply denies every device.
|
||||
|
||||
### The pairing code is not the credential
|
||||
|
||||
Three secrets are involved and conflating any two of them would break the model:
|
||||
|
||||
| value | who sees it | what it does |
|
||||
| --- | --- | --- |
|
||||
| pairing code (6 digits) | **anyone who can see the screen** | identifies the request to an administrator |
|
||||
| claim secret (256 bits) | only the requesting browser, HttpOnly | proves "I am the screen that asked" |
|
||||
| device credential (256 bits) | only the paired browser, HttpOnly | is the device's identity |
|
||||
|
||||
The code is printed across a television in a public hallway. It is therefore treated as public: it
|
||||
is stored in the clear, and knowing it is worth nothing on its own. Approving it requires an
|
||||
administrator holding `core.devices.manage`, and **collecting** the resulting credential requires the
|
||||
claim cookie, which no onlooker has. A flow that issued a credential to whoever presented the code
|
||||
would hand a screen's identity to anyone who read it off the wall.
|
||||
|
||||
Six digits are therefore not carrying the security of this design. They are bounded anyway: ten
|
||||
minutes, single-use, one live request per code enforced by a partial unique index, and an
|
||||
administrator in the loop.
|
||||
|
||||
### The permission comes from the display, never from the request
|
||||
|
||||
A pairing request names only the module whose display the screen is sitting on, and even that is not
|
||||
taken at its word — the module is looked up in the publication registry and the permission is read
|
||||
from its validated display declaration. A browser cannot ask to be granted `core.devices.manage`,
|
||||
another module's permission, or anything else. Approval grants exactly the one permission the host
|
||||
derived, never a bundle.
|
||||
|
||||
### State machine
|
||||
|
||||
`pending → approved → consumed`, plus expiry derived from `expires_at`.
|
||||
|
||||
Expiry is deliberately *not* a fourth state: nothing has to run for a code to stop working, so there
|
||||
is no sweeper whose failure would leave live codes lying around. Each transition is a conditional
|
||||
`UPDATE` — approval matches only a `pending`, unexpired row, and consumption matches only an
|
||||
`approved` one — so approving twice, approving something expired, and two screens racing to claim one
|
||||
approval all affect zero rows rather than racing. Exactly one credential can ever come out of one
|
||||
code.
|
||||
|
||||
### The credential is hashed, and revocation is server-side
|
||||
|
||||
The device receives an opaque 256-bit credential in an HttpOnly cookie; the server stores only
|
||||
SHA-256 of it. A fast hash is correct here — unlike a password, there is nothing to brute-force in
|
||||
CSPRNG output, and a slow KDF would only add latency to every request a display makes. The raw value
|
||||
exists in exactly two places: the response that sets the cookie, and the device's own cookie jar. It
|
||||
is never logged.
|
||||
|
||||
Revocation is `revoked_at` on the device row, checked on **every** principal resolution. It is not a
|
||||
request that a remote browser discard anything: a revoked screen keeps its cookie and stops working.
|
||||
|
||||
### Cookies
|
||||
|
||||
Two host-owned cookies, named nothing like Better Auth's: `ekklesia_device_pairing` (short-lived,
|
||||
cleared on use) and `ekklesia_device_session`. Both are `HttpOnly` — module display code runs in this
|
||||
very page and must never be able to read the credential — `SameSite=Strict`, host-only, `Path=/`, and
|
||||
`Secure` whenever the deployment's own base URL is https. Nothing about the credential is ever
|
||||
returned in JSON, put in a query string, or written to `localStorage`.
|
||||
|
||||
**A human session always wins.** The device cookie is not even read when Better Auth resolves a
|
||||
person, so an administrator who opens a display on their laptop stays themself.
|
||||
|
||||
### `/api/v1/principal` stays human-only
|
||||
|
||||
A device is reported as `anonymous` there, deliberately.
|
||||
|
||||
That endpoint is what the Dashboard shell uses to decide whether someone is signed in, and its
|
||||
shape — "not anonymous means logged in" — was load-bearing in the shell before this pass. Returning a
|
||||
device from it would have made a television a Dashboard user. The shell's test is now `=== "authenticated"`
|
||||
as a second lock, but the first lock is that the endpoint never says "device" at all.
|
||||
|
||||
The display host instead reads its viewer from the **descriptor**, which is the display's actual
|
||||
authorization boundary and already performed the check. So displays are descriptor-first while the
|
||||
Dashboard remains principal-first, and neither has to infer an identity by elimination.
|
||||
|
||||
### The display page is served to anyone; the descriptor is not
|
||||
|
||||
An unpaired screen has no identity, so a permission gate on the page would leave a new television
|
||||
with nowhere to render a code. The page is the same inert, module-agnostic bytes for every display —
|
||||
no module id, no data, one already-public script URL — and every authorization decision stays in the
|
||||
descriptor. What this does reveal to an anonymous caller is that a module with a display exists at
|
||||
that path, which any pairing flow must reveal to the screen it is pairing.
|
||||
|
||||
### Session lifetime
|
||||
|
||||
Device credentials last 180 days; human sessions are unchanged. A screen mounted near a ceiling
|
||||
should not need re-pairing on a fortnightly cycle. What makes a long horizon safe is that it is not
|
||||
the security boundary — the server re-checks revocation on every request — and the expiry exists so
|
||||
an abandoned, forgotten screen eventually stops being an identity at all. `lastSeenAt` is updated at
|
||||
most every 15 minutes, in SQL as well as in the resolver, so a display polling realtime does not
|
||||
become a write load.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Revocation is prompt, but not instantaneous, and this ADR says so
|
||||
|
||||
A revoked device fails its next HTTP request immediately, and its next realtime *connection*
|
||||
immediately. An **already open** SSE stream is not torn down at the moment of revocation: realtime
|
||||
already forces a reconnect every five minutes (ADR 0032), so the worst case is a bounded five-minute
|
||||
window during which a screen keeps receiving a channel it has just lost. Closing live streams on
|
||||
revocation would mean reaching into the realtime registry by principal, which is a change to a
|
||||
subsystem this pass has no other reason to touch. The bound is documented rather than described as
|
||||
instant.
|
||||
|
||||
### Re-pairing creates a new identity
|
||||
|
||||
A revoked screen that is paired again becomes a **new** device row with a new credential. The old
|
||||
credential is never reactivated, and the revoked row stays in the list as evidence. Reactivating the
|
||||
original would mean a revoked credential could come back, which is exactly what revocation must
|
||||
prevent.
|
||||
|
||||
### What was deliberately not built
|
||||
|
||||
QR codes (a future scanner can encode the same short code — the backend protocol does not change,
|
||||
and a QR must never carry the credential, an admin token, or a session), per-device content
|
||||
targeting, device groups, health monitoring, remote control, and mobile dashboards. The pairing API
|
||||
is not desktop-specific, so a future mobile dashboard host can consume it unchanged.
|
||||
|
||||
### Ekklesia Lite has none of this
|
||||
|
||||
Lite is a minimal trusted-network module runtime: module loading, config, database, storage, HTTP,
|
||||
realtime, and UI hosting. It deliberately provides no human authentication, no permission
|
||||
enforcement, and **no managed device identity or pairing** — a Lite-hosted display simply runs on a
|
||||
trusted local network. Device identity is a full-Ekklesia capability, and nothing here belongs in a
|
||||
future Lite contract. No module code branches on which host is running it.
|
||||
|
||||
### Module UI is untouched
|
||||
|
||||
A module's display receives a principal and its permissions, exactly as before. The principal union
|
||||
gained a `device` variant so a module can render honestly for a screen with nobody behind it, and
|
||||
that is the whole change — there are no pairing controls in `ModuleUiContext`, and a module cannot
|
||||
approve, list, or revoke anything.
|
||||
@@ -44,3 +44,4 @@ ADRs capture durable choices that affect more than one package or phase.
|
||||
- [0033: Display UI is a separate, permission-gated module entrypoint](0033-display-kiosk-host-contract.md) — Accepted
|
||||
- [0034: Dashboard browser authentication is host-owned and principal-first](0034-host-owned-browser-authentication.md) — Accepted
|
||||
- [0035: The first platform owner is created by an operator command, never by a browser](0035-initial-platform-owner-bootstrap.md) — Accepted
|
||||
- [0036: A display device is its own principal, paired by an administrator](0036-managed-device-identity-and-pairing.md) — Accepted
|
||||
|
||||
@@ -483,6 +483,18 @@ The permission must have an `authenticated` or `restricted` baseline: public/dev
|
||||
is still intentionally unsolved. The display artifact must be distinct from every other configured
|
||||
entrypoint. Primary UI, display UI, both, and neither are all valid module shapes.
|
||||
|
||||
Phase 7B pass 2 makes those screens principals in their own right
|
||||
([ADR 0036](decisions/0036-managed-device-identity-and-pairing.md)). The platform recognizes
|
||||
anonymous, authenticated human, and managed device. A device is deliberately not a user — no identity
|
||||
row, no password, and no satisfaction of the `authenticated` baseline — and its authority is decided
|
||||
by the one authorization evaluator, from an explicit permission granted to that exact device.
|
||||
Pairing is administrator-approved: a screen shows a short, public code that identifies its request,
|
||||
while the credential is issued only to the browser holding a separate HttpOnly claim secret and is
|
||||
stored only as a digest. Revocation is server-side and checked on every principal resolution;
|
||||
re-pairing mints a new identity rather than reviving a revoked one. Ekklesia Lite provides module
|
||||
runtime primitives and deliberately no human authentication, permission enforcement, or managed
|
||||
device identity, so pairing is a full-Ekklesia concern only.
|
||||
|
||||
The two browser roles share the existing `defineModuleUi({ id, version, mount })` export and exactly
|
||||
the same `ModuleUiHostContext`; role selection comes from separate artifacts, never a `mode`,
|
||||
`presentation.kind`, or host-identity branch. The canonical builder reads only
|
||||
|
||||
@@ -826,6 +826,43 @@ still the right choice when the response must be computed, transformed, or autho
|
||||
There are no signed **write** URLs: uploads go through a module route, where the module can validate
|
||||
what it is accepting.
|
||||
|
||||
## Display device pairing and management
|
||||
|
||||
Host-owned, and reachable by no module
|
||||
([ADR 0036](../architecture/decisions/0036-managed-device-identity-and-pairing.md)). Two sibling
|
||||
prefixes: `/api/v1/device-pairing` and `/api/v1/devices`.
|
||||
|
||||
**The screen's side** (no identity required; authority is the HttpOnly claim cookie the first call
|
||||
sets, never the code):
|
||||
|
||||
| route | purpose |
|
||||
| --- | --- |
|
||||
| `POST /api/v1/device-pairing/requests` | Body `{moduleId}`. Returns `{code, expiresAt, pollIntervalMs}` and sets `ekklesia_device_pairing`. The permission is read from that module's validated display declaration — a request cannot name one. |
|
||||
| `POST /api/v1/device-pairing/claims` | Polls. Returns `{status}`; on `paired` it sets `ekklesia_device_session` and clears the pairing cookie. |
|
||||
|
||||
**The administrator's side** (requires the restricted `core.devices.manage` and an exact trusted
|
||||
`Origin`):
|
||||
|
||||
| route | purpose |
|
||||
| --- | --- |
|
||||
| `POST /api/v1/device-pairing/lookups` | Body `{code}`. Shows the module and permission a screen is asking for, before approving. |
|
||||
| `POST /api/v1/device-pairing/approvals` | Body `{code, deviceName}`. The administrator names the screen; the screen never names itself. |
|
||||
| `GET /api/v1/devices` | Paired devices: name, permissions, created, last seen, revoked. Never a credential. |
|
||||
| `POST /api/v1/devices/:deviceId/revoke` | `204`. Server-side revocation, checked on every principal resolution. |
|
||||
|
||||
The pairing code travels in request **bodies**, never a path or query, so it does not land in access
|
||||
logs, browser history, or a `Referer`. Expired, consumed, mistyped, and never-existed codes all
|
||||
answer the same way.
|
||||
|
||||
`/api/v1/principal` deliberately reports a device as `anonymous`: it answers a question about
|
||||
*people*, and the Dashboard depends on that. A display reads its viewer from the display descriptor
|
||||
instead, which now carries `viewer: {kind: "authenticated", userId} | {kind: "device", deviceId}`.
|
||||
|
||||
**Cookies.** `ekklesia_device_session` is `HttpOnly`, `SameSite=Strict`, host-only, `Path=/`, and
|
||||
`Secure` when the identity base URL is https. Deployment note: because it is host-only and
|
||||
`SameSite=Strict`, a display must be served from the same origin as the API — which the platform's
|
||||
own `/display/<module-id>` route is.
|
||||
|
||||
## Module HTTP routes
|
||||
|
||||
An installed module's routes (declared via `http.route(...)` in its `register` hook — see
|
||||
|
||||
@@ -275,6 +275,30 @@ unregistered target permission instead receives the identical sanitized
|
||||
`403 {"code":"AUTHORIZATION_DENIED"}` a genuinely denied caller would, so that response never
|
||||
reveals whether the caller was denied or the permission was simply never registered.
|
||||
|
||||
## Device permissions
|
||||
|
||||
A paired display device holds explicit permissions of its own, in `ekklesia_core_device_permission`
|
||||
rather than in `ekklesia_core_grant`
|
||||
([ADR 0036](../architecture/decisions/0036-managed-device-identity-and-pairing.md)). That table's
|
||||
`user_id` is a non-null foreign key into the user table, and making it polymorphic would rewrite the
|
||||
platform's central authorization record to serve one narrow case.
|
||||
|
||||
**It is a separate table, not a separate decision.** `createAuthorizationEvaluator` consults exactly
|
||||
one authority per call, chosen by principal kind, and a device reaches `allowed` only by holding the
|
||||
exact permission being checked:
|
||||
|
||||
- `public` — allowed, as for any principal.
|
||||
- `authenticated` — **denied**, always. A device is not a person.
|
||||
- `restricted` — allowed only with an active, unrevoked permission for that exact device.
|
||||
|
||||
A device is never a platform owner, and the owner and user-grant ports are not consulted for one at
|
||||
all. A deployment that composed no device support denies every device. Revoking a device revokes its
|
||||
permissions with it, and the permission lookup joins the device row so a revoked device has no
|
||||
permissions even if a row were left behind.
|
||||
|
||||
Approving a pairing grants exactly the one display permission the host derived from the module's
|
||||
validated display declaration — never a bundle, never a permission the browser named.
|
||||
|
||||
## Platform-owner bootstrap
|
||||
|
||||
There is no "first user becomes owner" inference anywhere in this repository, and no HTTP
|
||||
|
||||
@@ -265,6 +265,20 @@ registry) is covered separately in [Authorization](authorization.md). Its bootst
|
||||
an existing identity; it does not create one. Creating the *first* identity on a fresh installation
|
||||
is what `pnpm identity:create-initial-owner` is for — see below.
|
||||
|
||||
## Three principal classes
|
||||
|
||||
Ekklesia recognizes **anonymous**, **authenticated** (a person), and **device** (a paired display)
|
||||
([ADR 0036](../architecture/decisions/0036-managed-device-identity-and-pairing.md)).
|
||||
|
||||
A device is not a user: it has no row in the identity tables, no email, no password, and no human
|
||||
session. `isAuthenticatedPrincipal` returns `false` for it, which is what keeps every "somebody is
|
||||
signed in" route closed to a screen on a wall. Its credential is a separate HttpOnly cookie
|
||||
(`ekklesia_device_session`), resolved after Better Auth has been asked first — a human session always
|
||||
wins, so a browser holding both remains a person.
|
||||
|
||||
`/api/v1/principal` reports a device as `anonymous` on purpose. It is the endpoint the Dashboard uses
|
||||
to decide whether a person is signed in, and a device is not one.
|
||||
|
||||
## Creating the first platform owner
|
||||
|
||||
A fresh installation has a login page and nothing to log in to. Public registration is disabled by
|
||||
|
||||
@@ -544,6 +544,44 @@ is **where it can be invoked**, not a weaker policy:
|
||||
It deliberately does not solve creating *additional* users or owners. That is user administration,
|
||||
which does not exist yet.
|
||||
|
||||
### A display device is a principal, not a person and not a shortcut
|
||||
|
||||
An unattended screen has its own identity, paired by an administrator and revocable by one
|
||||
([ADR 0036](../architecture/decisions/0036-managed-device-identity-and-pairing.md)). What makes that
|
||||
safe is what it is *not* allowed to be:
|
||||
|
||||
- **Not a user.** No device row exists in the identity tables and no device has an email or a
|
||||
password. A device satisfies the `authenticated` baseline **never**, so every route that settles
|
||||
for "somebody is signed in" stays closed to a television.
|
||||
- **Not a bypass.** Device authority is decided by the same `createAuthorizationEvaluator` as
|
||||
everyone else's, with the same deny-by-default. There is no device branch that returns allowed
|
||||
before consulting an explicit permission, and no device is ever a platform owner.
|
||||
- **Not broadly granted.** Approval grants exactly the one display permission the host derived from
|
||||
the module's own validated display declaration. A browser cannot ask for a permission of its
|
||||
choosing, and a device paired for one display has no authority over another.
|
||||
- **Not a Dashboard login.** `/api/v1/principal` reports a device as anonymous, and the shell tests
|
||||
for `=== "authenticated"` rather than "not anonymous". A device cookie cannot open the Dashboard.
|
||||
- **Not the pairing code.** The six-digit code is displayed on a wall and is treated as public. It
|
||||
identifies a request to an administrator; it never yields a credential. Collecting the credential
|
||||
additionally requires the HttpOnly claim cookie only the requesting browser holds, so reading a
|
||||
code off a screen is worth nothing.
|
||||
- **Not stored in a usable form.** The server keeps SHA-256 of the credential. The raw value exists
|
||||
only in the `Set-Cookie` that issues it and in the device's own cookie jar; it is never logged, put
|
||||
in a query string, returned in JSON, or written to `localStorage`. The cookie is `HttpOnly`, so the
|
||||
module display code running in that very page cannot read it.
|
||||
- **Not revocable only at the browser.** Revocation is a server-side column checked on every
|
||||
principal resolution. A revoked screen keeps its cookie and stops working; re-pairing creates a
|
||||
**new** identity, so an old credential can never be reactivated.
|
||||
|
||||
Approval, listing, and revocation require the restricted `core.devices.manage` permission and an
|
||||
exact trusted `Origin`, exactly like every other Core mutation. A device cannot approve a pairing —
|
||||
including its own.
|
||||
|
||||
**Bounded, not instantaneous.** A revoked device fails its next request and its next realtime
|
||||
connection immediately, but an already-open SSE stream is not torn down at that moment: realtime's
|
||||
five-minute forced reconnect is the upper bound on how long a just-revoked screen can keep receiving
|
||||
a channel. That window is stated rather than described as instant.
|
||||
|
||||
### Rate limiting is per-process today
|
||||
|
||||
Better Auth's authentication rate limiter is **in-memory and per API process**. That is acceptable
|
||||
|
||||
@@ -351,6 +351,30 @@ password arriving on standard input and never in `argv`, `--password` refused, a
|
||||
`DATABASE_URL`, and an unmigrated database refused rather than silently migrated — with assertions
|
||||
that neither stdout nor stderr contains the password or the connection string.
|
||||
|
||||
A further suite (`apps/api/tests/device-pairing.integration.ts`, same harness) proves display device
|
||||
pairing end to end against real PostgreSQL and the real HTTP transport
|
||||
([ADR 0036](../architecture/decisions/0036-managed-device-identity-and-pairing.md)). None of it is
|
||||
mocked: a screen with no identity is refused the descriptor but served the page it needs, asks to be
|
||||
paired, and receives a code; a real platform owner — created by the Pass 1B bootstrap and signed in
|
||||
through the real login transport — looks the code up and approves it by name; the screen collects its
|
||||
credential from a real `Set-Cookie` and then opens the real display descriptor and a real realtime
|
||||
stream; and the owner revokes it, after which both fail while the device still holds its cookie.
|
||||
|
||||
The suite also pins the properties that make the design safe rather than merely working: that the
|
||||
raw credential appears nowhere in the database or in any response body, that the permission is taken
|
||||
from the display declaration and not from a request that tried to name `core.devices.manage`, that
|
||||
someone who knows only the code (an onlooker reading a television) is issued nothing, that anonymous
|
||||
callers, a paired device, and a cross-origin administrator are all refused approval, that one code
|
||||
cannot be approved twice or claimed into two credentials under a race, that re-pairing produces a
|
||||
fresh identity while the old credential stays dead, and that a browser holding both a human session
|
||||
and a device cookie remains a person.
|
||||
|
||||
Device authority itself is proven without a database in `core/tests/authorization-device.test.ts`,
|
||||
which is mostly a list of things a device must *not* acquire: authority from being a principal, from
|
||||
an `authenticated` baseline, from platform ownership, or from a user grant. Its ports are wired to
|
||||
answer "yes" to every human question, so any accidental fall-through into a human path would flip
|
||||
those assertions.
|
||||
|
||||
### Which kind of timing proof lives where
|
||||
|
||||
Pass 4 introduced several deadlines, and their tests deliberately differ in kind:
|
||||
|
||||
Reference in New Issue
Block a user