fix(sharing): keep life events to their owner

Life events rode the profile sharing domain, so a profile share, a
legacy whole-record share and a MANAGE grant all read them, and MANAGE
could write them. No share level was ever worded for them, so in this
release they are the owner's only.

The life-event routes take a bare requireAuth, which refuses under an
acting-account switch. The day section and the timeline's life lane map
to no domain, so a delegate sees the section named not_shared and never
gets the lane, its readiness row or the day index marks. The profile
consent copy no longer names life events, the sharing guard and the
mutation inventory drop the four entries, and the OpenAPI descriptions
say owner-only.
This commit is contained in:
Marc-André Bombeck committed 2026-10-08 22:37:57 +02:00
1 parent e2ed62c74f
commit f950dffad4
21 files changed
+323 -196

No files matched your search

+6 -6
View File
@@ -1,8 +1,8 @@
/**
* `PATCH /api/life-events/{id}` edits one life event; `DELETE` soft-deletes
* it (v1.42, #613). Record-scoped like the list beside it, and MANAGE like
* the create: correcting or removing somebody's life event is a guardian's
* act. A foreign, unknown or already deleted id is the same 404.
* it (v1.42, #613). Owner-only like the list beside it (see there): no
* delegate at any level, and no guardian acting for a managed profile. A
* foreign, unknown or already deleted id is the same 404.
*
* The edit files the replaced dates, category and precision (C4) and names a
* changed title or note without quoting either. The delete tombstones the
@@ -10,7 +10,7 @@
*/
import { NextRequest } from "next/server";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
@@ -42,7 +42,7 @@ function notFound() {
export const PATCH = apiHandler(
async (request: NextRequest, { params }: RouteParams) => {
const { user } = await requireRecordAuth("manage", "profile");
const { user } = await requireAuth();
const path = lifeEventIdPathSchema.safeParse(await params);
if (!path.success) return notFound();
const existing = await findOwnLifeEvent(user.id, path.data.id);
@@ -109,7 +109,7 @@ export const PATCH = apiHandler(
export const DELETE = apiHandler(
async (request: NextRequest, { params }: RouteParams) => {
const { user } = await requireRecordAuth("manage", "profile");
const { user } = await requireAuth();
const path = lifeEventIdPathSchema.safeParse(await params);
if (!path.success) return notFound();
const existing = await findOwnLifeEvent(user.id, path.data.id);
+17 -13
View File
@@ -1,21 +1,25 @@
/**
* `GET /api/life-events` lists the record's life events, oldest first;
* `GET /api/life-events` lists the account's life events, oldest first;
* `POST` adds one (v1.42, #613). Title and note are encrypted at rest and
* never reach a model. Shapes and request schemas in `src/lib/day/`.
*
* Part of the `profile` sharing domain, beside allergies, visits and the
* immunization history. The row store answers whether or not the `timeline`
* module is on, the data-layer posture of the other record tables, so a
* restore keeps working and re-enabling finds the events intact.
* Owner-only in v1.42. A life event (a loss, a separation, a new job) is
* the most personal row the record holds, and no share level was ever
* worded for it, so no delegate reaches it: not a `profile` share, not a
* legacy whole-record share, not MANAGE, not a guardian acting for a
* managed profile. Bare `requireAuth()`, which refuses under an
* acting-account switch and refuses a narrow Bearer scope. Sharing life
* events is a separate decision for a later release.
*
* The create is a MANAGE verb: a life event is the person's own anchor, and
* recording one for somebody else is a guardian's act on a managed profile,
* not a helper's. Additive, rate-limited on the actor, audited with the
* category and the precision and never the title.
* The row store answers whether or not the `timeline` module is on, the
* data-layer posture of the other record tables, so a restore keeps working
* and re-enabling finds the events intact. The create is additive,
* rate-limited, audited with the category and the precision and never the
* title.
*/
import { NextRequest } from "next/server";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
@@ -35,7 +39,7 @@ import { annotate } from "@/lib/logging/context";
import { checkRecordWriteRateLimit } from "@/lib/rate-limit";
export const GET = apiHandler(async () => {
const { user } = await requireRecordAuth("read", "profile");
const { user } = await requireAuth();
const events = await listLifeEvents(user.id);
annotate({
action: { name: "life_event.list", entity_type: "life_event" },
@@ -47,9 +51,9 @@ export const GET = apiHandler(async () => {
export const POST = apiHandler(withIdempotency<[NextRequest]>(postLifeEvent));
async function postLifeEvent(request: NextRequest): Promise<Response> {
const { user, actor } = await requireRecordAuth("manage", "profile");
const { user } = await requireAuth();
const writeRl = await checkRecordWriteRateLimit(actor.id);
const writeRl = await checkRecordWriteRateLimit(user.id);
if (!writeRl.allowed) {
return apiError("Too many writes, try again later", 429, {
errorCode: "record_write.rate_limited",