Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/admin/persona.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export interface AttributePutParams extends PersonaHolderParams {
type: string;
valueType: AttributeValueType;
/**
* The fact itself. Must agree with `valueType`; the agent refuses a document
* The value itself. Must agree with `valueType`; the agent refuses a document
* where it does not.
*
* Typed `unknown` deliberately. The published schema places no type
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/persona/consent-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ function describePredicate(p: NonNullable<PreviewClaim["predicate"]>): string {
}[p.op] ?? p.op;
const arg = typeof p.arg === "string" ? p.arg : JSON.stringify(p.arg);
// The trailing clause is the point of the row, not a footnote: this rung is
// the only one where the verifier learns a fact without receiving a value.
// the only one where the verifier learns something without receiving a value.
return `proves ${op} ${arg} — the value itself is not sent`;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/persona/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// identity out, these record what came back the other way. A contact is stored
// **as received** and is never merged into the holder's own attributes — that
// separation is the point. A contact is somebody else's account of themselves,
// not a fact the holder is asserting, and a store that blurred the two would
// not an attribute the holder is asserting, and a store that blurred the two would
// let a peer's claim about their own name be re-presented as the holder's.
//
// Every contact is filed against `knownByPersona`: which of the holder's own
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/persona/step-up.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export interface DisclosureStepUpRequired {
* Named for what it is. Nothing in it may be shown to a human or signed over
* until {@link verifyDisclosureStepUp} has passed — the spec rule is that a
* consumer verifies the proof *before* surfacing the reason, and here the
* reason includes the list of facts about to leave.
* reason includes the list of attributes about to leave.
*/
unverifiedApproveRequest: Record<string, unknown>;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/core/tests/persona.step-up.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ async function approveRequest({ as = AGENT, previewId = PREVIEW, ctx = {} } = {}
subject: "did:key:zHolder",
sessionId: "sess-42",
challenge: "a".repeat(32),
reason: "Approve disclosing 1 fact to did:key:zVerifier",
reason: "Approve disclosing 1 attribute to did:key:zVerifier",
ext: {
[AUTHZ_EXT]: {
type: CONTEXT_TYPE,
summary: "Approve disclosing 1 fact to did:key:zVerifier",
summary: "Approve disclosing 1 attribute to did:key:zVerifier",
risk: "high",
action: {
kind: "disclose",
Expand Down Expand Up @@ -135,7 +135,7 @@ test("what the holder is shown comes out of the signature", async () => {
assert.deepEqual(res.context.claimTypes, ["payment.card"]);
assert.equal(res.context.verifierDid, "did:key:zVerifier");
assert.equal(res.context.purpose, "checkout");
assert.equal(res.context.summary, "Approve disclosing 1 fact to did:key:zVerifier");
assert.equal(res.context.summary, "Approve disclosing 1 attribute to did:key:zVerifier");
assert.equal(res.request.challenge, "a".repeat(32));
});

Expand Down
2 changes: 1 addition & 1 deletion packages/extension/src/confirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const isTaskConsent = params.get("kind") === "task";
// `kind=disclosure` selects the persona-disclosure surface. It is its own
// surface rather than a variant of the login prompt because what it authorizes
// is different in kind: the login prompt says "let this site sign you in", and
// this one says "let this verifier keep these facts about you". Sharing a
// this one says "let this verifier keep these attributes about you". Sharing a
// component would mean one screen trying to word both.
const isDisclosureConsent = params.get("kind") === "disclosure";
// The biometric-gated approver surface: Approve must run a fresh WebAuthn
Expand Down
2 changes: 1 addition & 1 deletion packages/extension/src/disclosure-consent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
// - **Withheld rows are drawn as warnings, not as claims.** They are the only
// rows whose presence makes the disclosure smaller than it looks.
// - **A predicate is drawn in the positive colour.** It is the strongest
// outcome on the screen — the verifier learns a fact and receives no value
// outcome on the screen — the verifier learns something and receives no value
// — and drawing it in the same ink as an ordinary disclosure would hide the
// one row where the holder gave away least.
// - **Linkability leads when it is not `none`.** It sits above the claim list
Expand Down
8 changes: 4 additions & 4 deletions packages/extension/src/manager/claim-sensitivity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// How carefully a fact's value is shown to the person who owns it.
// How carefully an attribute's value is shown to the person who owns it.
//
// ## This is not a security control, and saying so is the point
//
Expand Down Expand Up @@ -133,7 +133,7 @@ const REGISTERED: Readonly<Record<string, ClaimTreatment>> = {
* §4's first rule is a per-attribute override the holder set explicitly, which
* wins over the registry. No field carries one on the wire yet, so nothing here
* can read it; when one exists it belongs *above* this call, not inside it,
* because "the holder decided" and "the registry says" are different facts and
* because "the holder decided" and "the registry says" are different attributes and
* a UI that wants to explain the difference needs both.
*
* **The prefix walk is rule 3, and it only ever tightens.** An unregistered
Expand Down Expand Up @@ -274,12 +274,12 @@ function emailLocal(text: string): string {
}

/**
* What to draw for a fact, and whether a *Show* control belongs beside it.
* What to draw for an attribute, and whether a *Show* control belongs beside it.
*
* `masked` is the caller's cue for two separate things and both matter: a
* reveal control, and a rendering distinct from an absent value. A pane that
* greys a mask the way it greys "not requested" has told the operator that a
* fact they hold is a fact they do not.
* attribute they hold is an attribute they do not.
*/
export function maskedFact(type: string, text: string): { text: string; masked: boolean } {
const treatment = treatmentOf(type);
Expand Down
2 changes: 1 addition & 1 deletion packages/extension/src/manager/holder-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function holderGate(authority: Authority | null): string | null {
if (!authority) return null;
if (isUnscopedHolder(authority)) return null;
return (
"Your facts sit above every context, so reaching them takes authority of its own: " +
"Your attributes sit above every context, so reaching them takes authority of its own: " +
"an agent credential with no context restriction, or one granted the " +
"`persona-holder` capability. This console cannot see which you have — " +
"`auth/whoami` reports roles and scopes, not capabilities — so it does not stop " +
Expand Down
54 changes: 27 additions & 27 deletions packages/extension/src/manager/identity-graph.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
// The identity map's model: facts, faces, contexts, and what reaches what.
// The identity map's model: attributes, faces, contexts, and what reaches what.
//
// The persona pane draws one picture of the whole model — facts on top, faces
// The persona pane draws one picture of the whole model — attributes on top, faces
// in the middle, contexts below — and lights up everything a selection reaches.
// Which things light up is the substance of the picture: a fact that reaches
// Which things light up is the substance of the picture: an attribute that reaches
// two contexts through one face is the holder's linkage made visible, and a
// context that lights the wrong face is a lie about what it holds. So the graph
// and the reach computation live here, out of the component, with no DOM and
// no relative imports, where they can be tested the way `profile-entries.ts`
// was.
//
// Vocabulary on screen follows `design-docs/persona-vocabulary.md`: a fact, a
// Vocabulary on screen follows `design-docs/persona-vocabulary.md`: an attribute, a
// face, a context, a persona that *wears* a face. The identifiers below keep the
// spec's names where they name wire records (`profileId`, `personaDid`).

import type { PoolAttribute, PoolProfile, PoolProfileEntry } from "@openvtc/pnm-core/admin";

export interface FactNode {
export interface AttributeNode {
id: string;
type: string;
label?: string | undefined;
Expand All @@ -29,10 +29,10 @@ export interface FactNode {
export interface FaceNode {
id: string;
name: string;
/** Facts this face selects by live reference. */
factIds: string[];
/** Attributes this face selects by live reference. */
attributeIds: string[];
/** Entries that are not a live reference — pinned, overridden, inline. They
* still reach a context; they just do not draw to a fact card. */
* still reach a context; they just do not draw to an attribute card. */
preserved: number;
entries: PoolProfileEntry[];
version: number;
Expand Down Expand Up @@ -65,14 +65,14 @@ export interface FaceLink {
}

export interface IdentityGraph {
facts: FactNode[];
attributes: AttributeNode[];
faces: FaceNode[];
contexts: ContextNode[];
links: FaceLink[];
}

export type Selection =
| { kind: "fact"; id: string }
| { kind: "attribute"; id: string }
| { kind: "face"; id: string }
| { kind: "context"; id: string }
| { kind: "persona"; contextId: string; did: string };
Expand Down Expand Up @@ -111,7 +111,7 @@ export function buildGraph(
profiles: readonly PoolProfile[],
contexts: readonly ContextInput[],
): IdentityGraph {
const facts: FactNode[] = attributes.map((a) => ({
const attributeNodes: AttributeNode[] = attributes.map((a) => ({
id: a.attributeId,
type: a.type,
label: a.label,
Expand All @@ -127,7 +127,7 @@ export function buildGraph(
return {
id: p.profileId,
name: p.name,
factIds: refs,
attributeIds: refs,
preserved: p.entries.length - refs.length,
entries: p.entries,
version: p.version,
Expand Down Expand Up @@ -166,28 +166,28 @@ export function buildGraph(
.filter(([, list]) => list.length > 1)
.map(([faceId, list]) => ({ faceId, wearers: list }));

return { facts, faces, contexts: contextNodes, links };
return { attributes: attributeNodes, faces, contexts: contextNodes, links };
}

/** Everything a selection reaches, in every direction it can reach. */
export interface Reach {
factIds: Set<string>;
attributeIds: Set<string>;
faceIds: Set<string>;
personaKeys: Set<string>;
contextIds: Set<string>;
}

function empty(): Reach {
return { factIds: new Set(), faceIds: new Set(), personaKeys: new Set(), contextIds: new Set() };
return { attributeIds: new Set(), faceIds: new Set(), personaKeys: new Set(), contextIds: new Set() };
}

/**
* What lights up.
*
* Reach runs **downwards from a fact** — the faces that select it, the personas
* Reach runs **downwards from an attribute** — the faces that select it, the personas
* that wear those faces, the contexts they are in — and **upwards from a
* context** — its personas' faces and those faces' facts. A face reaches both
* ways. That asymmetry is the model: a fact's reach is where it *goes*; a
* context** — its personas' faces and those faces' attributes. A face reaches both
* ways. That asymmetry is the model: an attribute's reach is where it *goes*; a
* context's reach is what it *holds*.
*
* Nothing here reads a context's copy. The picture is drawn from the pool and
Expand All @@ -199,7 +199,7 @@ export function reachOf(graph: IdentityGraph, selection: Selection | null): Reac
const out = empty();
if (!selection) return out;

const facesWithFact = (factId: string) => graph.faces.filter((f) => f.factIds.includes(factId));
const facesWithAttribute = (attributeId: string) => graph.faces.filter((f) => f.attributeIds.includes(attributeId));
const wearersOf = (faceId: string) =>
graph.contexts.flatMap((c) => c.personas.filter((p) => p.faceId === faceId));

Expand All @@ -213,13 +213,13 @@ export function reachOf(graph: IdentityGraph, selection: Selection | null): Reac
const lightFaceUp = (faceId: string) => {
out.faceIds.add(faceId);
const face = graph.faces.find((f) => f.id === faceId);
for (const id of face?.factIds ?? []) out.factIds.add(id);
for (const id of face?.attributeIds ?? []) out.attributeIds.add(id);
};

switch (selection.kind) {
case "fact":
out.factIds.add(selection.id);
for (const face of facesWithFact(selection.id)) lightFaceDown(face.id);
case "attribute":
out.attributeIds.add(selection.id);
for (const face of facesWithAttribute(selection.id)) lightFaceDown(face.id);
break;
case "face":
lightFaceUp(selection.id);
Expand All @@ -246,13 +246,13 @@ export function reachOf(graph: IdentityGraph, selection: Selection | null): Reac
return out;
}

/** The faces a fact reaches, and the contexts beyond them — the sentence the
/** The faces an attribute reaches, and the contexts beyond them — the sentence the
* detail strip says: "reaches 2 contexts through Glenn – Developer". */
export function factReach(
export function attributeReach(
graph: IdentityGraph,
factId: string,
attributeId: string,
): { faces: FaceNode[]; contextIds: string[]; wearers: PersonaNode[] } {
const faces = graph.faces.filter((f) => f.factIds.includes(factId));
const faces = graph.faces.filter((f) => f.attributeIds.includes(attributeId));
const wearers = graph.contexts.flatMap((c) =>
c.personas.filter((p) => p.faceId !== null && faces.some((f) => f.id === p.faceId)),
);
Expand Down
Loading
Loading