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
14 changes: 4 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@openvtc/pnm-core",
"version": "0.9.1",
"description": "Browser-side bridge between WebAuthn passkeys and VTA-managed DIDs. Wire types, WebAuthn ceremony helpers, COSE→Multikey conversion, DID verificationMethod builder, REST + DIDComm transports, mediator client, SIOP / RP-login / provision-integration flows.",
"description": "Browser-side bridge between WebAuthn passkeys and VTA-managed DIDs. Wire types, WebAuthn ceremony helpers, COSE\u2192Multikey conversion, DID verificationMethod builder, REST + DIDComm transports, mediator client, SIOP / RP-login / provision-integration flows.",
"license": "Apache-2.0",
"repository": {
"type": "git",
Expand Down Expand Up @@ -130,7 +130,7 @@
"dependencies": {
"@cfworker/json-schema": "^4.1.1",
"@noble/curves": "^2.4.0",
"@openvtc/trust-tasks": "^0.18.3",
"@openvtc/trust-tasks": "^0.19.5",
"@openvtc/vti-didcomm-js": "^0.8.0",
"@openvtc/vti-tsp-js": "^0.2.0",
"@scure/base": "^2.2.0",
Expand Down
35 changes: 35 additions & 0 deletions packages/core/src/webvh/dids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ import {
type Payload as DidsListPayload,
type Response as DidsListResponse,
} from "@openvtc/trust-tasks/vta/webvh/dids/list/1.0/payload";
import {
TYPE_URI as DIDS_REALIGN_KEYS,
RESPONSE_TYPE_URI as DIDS_REALIGN_KEYS_RESPONSE,
type Payload as DidsRealignKeysPayload,
type Response as DidsRealignKeysResponse,
} from "@openvtc/trust-tasks/vta/webvh/dids/realign-keys/1.0/payload";
import {
TYPE_URI as DIDS_UPDATE,
RESPONSE_TYPE_URI as DIDS_UPDATE_RESPONSE,
Expand Down Expand Up @@ -274,3 +280,32 @@ export async function webvhDidRegisterWithServer(
DIDS_REGISTER_WITH_SERVER_RESPONSE,
"vta/webvh/dids/register-with-server/1.0", payload);
}

/**
* Rename a DID's key records onto the verification-method ids its published
* document declares.
*
* **The repair for records the agent named without reading its own document.**
* A key record's id is a verification-method id — it is what a mediator matches
* an inbound JWE recipient against, and what anyone reading the document would
* hand to a signing task. Where the two disagree, the agent holds a key the
* document addresses under a name the agent does not answer to.
*
* `keys/rename` cannot fix that and is not meant to: its identifier gate refuses
* `:` and `#` so that a rename is not a way to write verification-method-shaped
* names into a key store. So this call names nothing — `did` and `dryRun` are
* the whole payload, and the agent derives every target from that DID's own
* published log, matching records to methods by public key.
*
* **Send it once with `dryRun` before sending it without.** The response is the
* same shape either way and says which it is, so a caller can show the plan and
* then apply exactly what was shown.
*/
export async function webvhDidRealignKeys(
sender: TrustTaskSender,
params: WebvhCall & DidsRealignKeysPayload,
): Promise<DidsRealignKeysResponse> {
const { holder, service, ...payload } = params;
return send(sender, { holder, service }, DIDS_REALIGN_KEYS, DIDS_REALIGN_KEYS_RESPONSE,
"vta/webvh/dids/realign-keys/1.0", payload);
}
76 changes: 75 additions & 1 deletion packages/core/task-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$comment": "Generated by scripts/sync-task-surface.mjs from a vta-sdk checkout. Do not hand-edit: re-run the script. Checked by tests/task-surface.mjs.",
"source": {
"crate": "vta-sdk",
"version": "0.35.0",
"version": "0.38.0",
"scanned": "vta-sdk/src/**/*.rs"
},
"tasks": [
Expand Down Expand Up @@ -56,6 +56,12 @@
"TASK_AUDIT_LIST_0_1"
]
},
{
"uri": "https://trusttasks.org/spec/audit/verify/0.1",
"consts": [
"TASK_AUDIT_VERIFY_0_1"
]
},
{
"uri": "https://trusttasks.org/spec/auth/authenticate/0.1",
"consts": [
Expand Down Expand Up @@ -1021,6 +1027,26 @@
"TASK_VAULT_UPSERT_0_3"
]
},
{
"uri": "https://trusttasks.org/spec/vetting/decline/0.1",
"consts": [
"VETTING_DECLINE_TYPE"
]
},
{
"uri": "https://trusttasks.org/spec/vetting/request/0.1",
"consts": [
"VETTING_REQUEST_RESPONSE_TYPE",
"VETTING_REQUEST_TYPE"
]
},
{
"uri": "https://trusttasks.org/spec/vetting/session/0.1",
"consts": [
"VETTING_SESSION_RESPONSE_TYPE",
"VETTING_SESSION_TYPE"
]
},
{
"uri": "https://trusttasks.org/spec/vta/app-state/delete/1.0",
"consts": [
Expand Down Expand Up @@ -1381,6 +1407,12 @@
"TASK_WEBVH_DIDS_LIST_1_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/webvh/dids/realign-keys/1.0",
"consts": [
"TASK_WEBVH_DIDS_REALIGN_KEYS_1_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/webvh/dids/register-with-server/1.0",
"consts": [
Expand Down Expand Up @@ -1443,6 +1475,13 @@
"JOIN_REQUEST_MANIFEST_TYPE"
]
},
{
"uri": "https://trusttasks.org/spec/vtc/join-requests/manifest/0.2",
"consts": [
"JOIN_REQUEST_MANIFEST_0_2_RESPONSE_TYPE",
"JOIN_REQUEST_MANIFEST_0_2_TYPE"
]
},
{
"uri": "https://trusttasks.org/spec/vtc/join-requests/status/0.1",
"consts": [
Expand Down Expand Up @@ -1494,6 +1533,41 @@
"MEMBER_VMC_RESPONSE_TYPE",
"MEMBER_VMC_TYPE"
]
},
{
"uri": "https://trusttasks.org/spec/vtc/vetting/revoke-statement/0.1",
"consts": [
"VETTING_REVOKE_STATEMENT_RESPONSE_TYPE",
"VETTING_REVOKE_STATEMENT_TYPE"
]
},
{
"uri": "https://trusttasks.org/spec/vtc/vetting/vetters/grant/0.1",
"consts": [
"VETTING_VETTER_GRANT_RESPONSE_TYPE",
"VETTING_VETTER_GRANT_TYPE"
]
},
{
"uri": "https://trusttasks.org/spec/vtc/vetting/vetters/list/0.1",
"consts": [
"VETTING_VETTER_LIST_RESPONSE_TYPE",
"VETTING_VETTER_LIST_TYPE"
]
},
{
"uri": "https://trusttasks.org/spec/vtc/vetting/vetters/profile/0.1",
"consts": [
"VETTING_VETTER_PROFILE_RESPONSE_TYPE",
"VETTING_VETTER_PROFILE_TYPE"
]
},
{
"uri": "https://trusttasks.org/spec/vtc/vetting/vetters/resend/0.1",
"consts": [
"VETTING_VETTER_RESEND_RESPONSE_TYPE",
"VETTING_VETTER_RESEND_TYPE"
]
}
]
}
15 changes: 14 additions & 1 deletion packages/core/tests/task-surface.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,20 @@ test("coverage against the agent's surface is recorded, not discovered", () => {
// address (dtgwg-trust-tasks-tf#426). The third is `rooms/keys/present/0.2`
// and `rooms/owner/issue-authority/0.2` counting as new families beside the
// 0.1 this library had drifted behind.
const expected = 204;
//
// 204 → 205 is `vta/webvh/dids/realign-keys/1.0` — the repair for a DID whose
// key records are not named after the verification methods it publishes
// (dtgwg-trust-tasks-tf#456, VTI#1466 and #1470).
//
// **The canonical total jumps 227 → 235 in the same commit, and only one of
// those eight is this task.** The other seven were always in the SDK and
// invisible to the scanner: `vta-sdk` derives a growing number of constants
// from the generated payload type rather than writing the URI out, and
// `sync-task-surface.mjs` matched only literals until it was taught to
// resolve them. So this is not the agent growing seven families — it is a
// snapshot that had been under-counting the denominator, and with it the gap
// this number exists to keep reviewable.
const expected = 205;
assert.equal(
implemented.size,
expected,
Expand Down
39 changes: 35 additions & 4 deletions packages/extension/src/manager/destructive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@
// because the agent refuses a non-empty deletion on purpose and overriding that
// refusal is a second decision.
//
// ## `nature`, and why a second component was not written
//
// Not every two-step action is a destruction. Realigning a DID's key records
// asks the agent for the same thing — what would change, before it changes —
// and then applies it, but nothing is lost: records are renamed and the key
// material behind them is untouched.
//
// Drawing that in red would be a lie of exactly the kind `manager-theme.css`
// forbids, where `--w-danger` is one of three colours that mean something. So
// the *shape* of the decision is shared and the **voice** is a parameter:
// `nature: "corrective"` asks what would change rather than what would be
// destroyed, and draws in the accent rather than in danger. It gets no `force`
// tick, because there is no refusal to override.
//
// ## Why `consentRequired` renders here
//
// The agent may answer "a human must approve this first". That is not a
Expand Down Expand Up @@ -77,6 +91,15 @@ export function ConsentCeremony({ pending }: { pending: ConsentRequiredError })
export interface DestructiveProps<P> {
/** Button copy for the action itself, e.g. "Delete context". */
label: string;
/**
* What kind of action this is, which decides the voice and the colour.
*
* `destructive` (the default) is the one this component was written for:
* something is lost, so the preview asks what would be destroyed and is drawn
* in danger. `corrective` is a repair — the preview asks what would change,
* and drawing it in danger would overstate it.
*/
nature?: "destructive" | "corrective";
/** Disabled reason, or null when the action is available. Shown rather than
* hiding the control — see `hasRole` in `use-vta.ts`. */
disabledReason?: string | null;
Expand Down Expand Up @@ -112,6 +135,7 @@ type Phase<P> =
*/
export function Destructive<P>({
label,
nature = "destructive",
disabledReason = null,
preview,
renderPreview,
Expand All @@ -122,6 +146,7 @@ export function Destructive<P>({
}: DestructiveProps<P>) {
const [phase, setPhase] = useState<Phase<P>>({ kind: "idle" });
const [force, setForce] = useState(false);
const corrective = nature === "corrective";

const reset = useCallback(() => {
setPhase({ kind: "idle" });
Expand Down Expand Up @@ -162,7 +187,7 @@ export function Destructive<P>({
if (phase.kind === "idle") {
return (
<Button
kind="danger"
kind={corrective ? "quiet" : "danger"}
disabled={Boolean(disabledReason)}
{...(disabledReason ? { title: disabledReason } : {})}
onClick={() => void start()}
Expand All @@ -173,7 +198,13 @@ export function Destructive<P>({
}

if (phase.kind === "previewing") {
return <span style={{ fontSize: t.sm, color: c.muted }}>Asking your agent what this would destroy…</span>;
return (
<span style={{ fontSize: t.sm, color: c.muted }}>
{corrective
? "Asking your agent what this would change…"
: "Asking your agent what this would destroy…"}
</span>
);
}

if (phase.kind === "consent") {
Expand Down Expand Up @@ -217,7 +248,7 @@ export function Destructive<P>({

return (
<div style={{ display: "grid", gap: 12, maxWidth: 460 }}>
<Note tone="danger">
<Note tone={corrective ? "accent" : "danger"}>
<div style={{ display: "grid", gap: 8 }}>{renderPreview(p)}</div>
</Note>

Expand All @@ -234,7 +265,7 @@ export function Destructive<P>({
)}

<div style={{ display: "flex", gap: 8 }}>
<Button kind="danger" disabled={blocked || busy} onClick={() => void run(p)}>
<Button kind={corrective ? "primary" : "danger"} disabled={blocked || busy} onClick={() => void run(p)}>
{busy ? "Working…" : label}
</Button>
<Button kind="quiet" disabled={busy} onClick={reset}>
Expand Down
Loading