Split what a person is from how they sign in and how they are seen #55

Merged
PerishFire merged 1 commit from land/identity-face into main 2026-07-31 04:18:50 +00:00
Owner

identity: split what a person is from how they sign in and how they are seen

Actor.login was doing two jobs. It was the account name a password is
checked against, and it was the handle the world knows someone by. Those
are different facts with different lifetimes and different audiences, and
holding them in one unique field means a second login source cannot be
added without breaking the first.

Three units now, where there was one and a half.

Actor carries what ensign decides about a person: an opaque sub, a
kind, and whether they are barred. Profile carries what the world may
see — a handle and a name — and is veiled, so it has no generic routes and
cannot be listed. Source carries one way of signing in: a kind and the
handle within that kind, unique per kind, many to one at the Actor. Pass
roots at the Source rather than the Actor, because a password belongs to
the password login and not to the person.

The subject is opaque and minted once. It was the Actor's row id, which
meant rebuilding an estate silently reassigned people to each other's
downstream records, and meant a directory could be walked by counting.
Neither is true of a value drawn from the CSPRNG.

Two ceremonies replace what the generic projection used to answer:

  • GET /api/profile/{sub} needs no credential and returns a handle and a
    name. Teams are not in it — group membership is not public.
  • GET /api/handle/{handle} needs an operator or the sudo token and
    returns the actor behind an exact handle.

Both take a key and return a row. Neither takes a predicate and neither
lists, so there is no enumeration face to close.

The service seat stops pretending. It was an Actor with a login it never
used; it is now an Actor with no Source at all, so being unable to sign
in is a fact of its shape rather than a convention.

Found on the way: repass passed an actor id where a source id was now
expected, and both are i64, so it compiled and would have refused every
password change. The custom routes also could not see the sudo face at
all — keel's admit accepts sudo, operator, or anon, but a handler
outside the generic projection only receives the operator, so crown was
turned away holding the highest credential there is. It now asks
core.seal, which is the same judgement keel makes.

identity: split what a person is from how they sign in and how they are seen `Actor.login` was doing two jobs. It was the account name a password is checked against, and it was the handle the world knows someone by. Those are different facts with different lifetimes and different audiences, and holding them in one unique field means a second login source cannot be added without breaking the first. Three units now, where there was one and a half. `Actor` carries what ensign decides about a person: an opaque `sub`, a kind, and whether they are barred. `Profile` carries what the world may see — a handle and a name — and is veiled, so it has no generic routes and cannot be listed. `Source` carries one way of signing in: a kind and the handle within that kind, unique per kind, many to one at the Actor. `Pass` roots at the Source rather than the Actor, because a password belongs to the password login and not to the person. The subject is opaque and minted once. It was the Actor's row id, which meant rebuilding an estate silently reassigned people to each other's downstream records, and meant a directory could be walked by counting. Neither is true of a value drawn from the CSPRNG. Two ceremonies replace what the generic projection used to answer: - `GET /api/profile/{sub}` needs no credential and returns a handle and a name. Teams are not in it — group membership is not public. - `GET /api/handle/{handle}` needs an operator or the sudo token and returns the actor behind an exact handle. Both take a key and return a row. Neither takes a predicate and neither lists, so there is no enumeration face to close. The service seat stops pretending. It was an Actor with a login it never used; it is now an Actor with no `Source` at all, so being unable to sign in is a fact of its shape rather than a convention. Found on the way: `repass` passed an actor id where a source id was now expected, and both are `i64`, so it compiled and would have refused every password change. The custom routes also could not see the sudo face at all — keel's `admit` accepts sudo, operator, or anon, but a handler outside the generic projection only receives the operator, so `crown` was turned away holding the highest credential there is. It now asks `core.seal`, which is the same judgement keel makes.
Split what a person is from how they sign in and how they are seen
All checks were successful
guard / guard (pull_request) Successful in 3m35s
guard / guard (push) Successful in 3m18s
0b93c9bbea
identity: split what a person is from how they sign in and how they are seen

`Actor.login` was doing two jobs. It was the account name a password is
checked against, and it was the handle the world knows someone by. Those
are different facts with different lifetimes and different audiences, and
holding them in one unique field means a second login source cannot be
added without breaking the first.

Three units now, where there was one and a half.

`Actor` carries what ensign decides about a person: an opaque `sub`, a
kind, and whether they are barred. `Profile` carries what the world may
see — a handle and a name — and is veiled, so it has no generic routes and
cannot be listed. `Source` carries one way of signing in: a kind and the
handle within that kind, unique per kind, many to one at the Actor. `Pass`
roots at the Source rather than the Actor, because a password belongs to
the password login and not to the person.

The subject is opaque and minted once. It was the Actor's row id, which
meant rebuilding an estate silently reassigned people to each other's
downstream records, and meant a directory could be walked by counting.
Neither is true of a value drawn from the CSPRNG.

Two ceremonies replace what the generic projection used to answer:

- `GET /api/profile/{sub}` needs no credential and returns a handle and a
  name. Teams are not in it — group membership is not public.
- `GET /api/handle/{handle}` needs an operator or the sudo token and
  returns the actor behind an exact handle.

Both take a key and return a row. Neither takes a predicate and neither
lists, so there is no enumeration face to close.

The service seat stops pretending. It was an Actor with a login it never
used; it is now an Actor with no `Source` at all, so being unable to sign
in is a fact of its shape rather than a convention.

Found on the way: `repass` passed an actor id where a source id was now
expected, and both are `i64`, so it compiled and would have refused every
password change. The custom routes also could not see the sudo face at
all — keel's `admit` accepts sudo, operator, or anon, but a handler
outside the generic projection only receives the operator, so `crown` was
turned away holding the highest credential there is. It now asks
`core.seal`, which is the same judgement keel makes.

Land-Source: identity-face@edae43013197323b5d371dbfae44a649b565cd56
PerishFire deleted branch land/identity-face 2026-07-31 04:18:51 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
PerishLab/ensign!55
No description provided.