feat(sharing): publish writable and manageable domains on the account payload

Every accountAccess entry on GET /api/auth/me carries writableDomains
and manageableDomains: the sections the grant opens, intersected with
the sections whose routes accept a delegated write at that level. A
READ grant gets two empty lists, a WRITE grant gets writable sections
only, a MANAGE grant gets both, and the vault appears in neither.

The client parser checks the structural invariants (no repeats, within
the grant's sections, manageable within writable, nothing below the
level) and requires the two published views of the active record to
agree on the lists. Regenerate the OpenAPI document and extend the
payload consumer guard with the active-record readers.
This commit is contained in:
Marc-André Bombeck committed 2026-09-09 19:42:26 +02:00
1 parent 54f0030ead
commit b5c392636f
14 files changed
+319 -2

No files matched your search

@@ -13,6 +13,8 @@ const OWNER = {
sections: null,
recordKind: "shared" as const,
canWrite: true,
writableDomains: [],
manageableDomains: [],
};
const OWN_RECORD: AccountAccess = {
@@ -127,6 +127,7 @@ vi.mock("@/components/gamification/achievement-unlock-notifier", () => ({
}));
import { AuthShell } from "../auth-shell";
import { delegatedDomains } from "@/lib/sharing/domain-write-support";
const OWNER_READ: AccountAccessEntry = {
accountId: "acct-owner",
@@ -138,12 +139,16 @@ const OWNER_READ: AccountAccessEntry = {
sections: null,
recordKind: "shared" as const,
canWrite: false,
writableDomains: [],
manageableDomains: [],
};
const OWNER_WRITE: AccountAccessEntry = {
...OWNER_READ,
access: "write",
level: "write",
canWrite: true,
writableDomains: [],
manageableDomains: [],
};
const MANAGED_GUARDIAN: AccountAccessEntry = {
accountId: "managed-record",
@@ -155,6 +160,8 @@ const MANAGED_GUARDIAN: AccountAccessEntry = {
sections: null,
recordKind: "managed" as const,
canWrite: true,
writableDomains: delegatedDomains("manage", null, "write"),
manageableDomains: delegatedDomains("manage", null, "manage"),
};
function render(
@@ -62,6 +62,8 @@ const OWNER = {
sections: null,
recordKind: "shared" as const,
canWrite: false,
writableDomains: [],
manageableDomains: [],
};
function render(access: AccountAccess, node: React.ReactNode): string {
@@ -37,6 +37,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { I18nProvider } from "@/lib/i18n/context";
import { ApiError } from "@/lib/api/api-fetch";
import type { AccountAccessEntry } from "@/lib/sharing/account-access-view";
import { delegatedDomains } from "@/lib/sharing/domain-write-support";
import type { GrantList } from "@/lib/queries/use-account-grants";
import type { ManagedProfileGuardian } from "@/lib/queries/use-managed-profiles";
@@ -219,6 +220,8 @@ function entry(partial: Partial<AccountAccessEntry> = {}): AccountAccessEntry {
recordKind: "managed",
sections: null,
canWrite: true,
writableDomains: delegatedDomains("manage", null, "write"),
manageableDomains: delegatedDomains("manage", null, "manage"),
...partial,
};
}