# Projects and Management membership

A project groups encrypted drops under one shared Management membership.
Create a project, retain its `data.project.id`, then send that value as
`project_id` on every [drop lifecycle](https://m7.org/docs/api/api.drop.m7.org/drop-lifecycle.md) request.

## Authentication and roles

Send an admitted M7 access token and JSON body. Project/drop routes use the
personal workspace; organization headers and selectors fail with HTTP `400`.
Authority requires the active, pinned Management association and active caller
membership. Another group's membership or a service admin/root role grants
no project authority. Each operation also requires its exact current capability.

| Project role | Allowed actions within its capability profile |
| --- | --- |
| `owner` | Read and mutate drops, rename the project, manage memberships and privileged roles, and delete the project. |
| `admin` | Read and mutate drops, rename the project, and manage member/viewer memberships. |
| `member` | Read and mutate drops. |
| `viewer` | Read project/drop metadata and keyed payloads; leave its membership. |

Every role can leave. The final active owner cannot leave, be disabled,
removed, or demoted. The `capabilities` returned by the project context are
the server's current action list; use them to enable UI controls.

## Routes and fields

All routes use `POST`. Unknown fields and caller-supplied authority are rejected.
`project_id` and `principal_id` are non-nil dashed UUIDs.

| Route | Required fields | Optional fields and result |
| --- | --- | --- |
| `/v1/project/create` | `name` | `description`; creates the project and makes the caller owner. Returns project context. |
| `/v1/project/list` | None | `q`, `limit`, `offset`; returns accessible projects. |
| `/v1/project/get` | `project_id` | Boolean `include_deletion_summary`; returns project context and an authorized deletion summary when requested. |
| `/v1/project/update` | `project_id`, `name` | `description`; owner/admin plus `project.update` required. |
| `/v1/project/delete` | `project_id`, `confirmed: true`, `summary` | Owner plus `project.delete` required; cascades to every project drop. |
| `/v1/project/member/list` | `project_id` | Owner/admin plus `project.member.list`; returns `data.items`. |
| `/v1/project/member/add` | `project_id`, `principal_id` | `role`, default `member` for a new membership; target must already be an active receiving principal. |
| `/v1/project/member/update` | `project_id`, `principal_id`, at least one change | `role` and/or `status` (`active` or `disabled`). |
| `/v1/project/member/remove` | `project_id`, `principal_id` | Marks membership removed and immediately ends its access. |
| `/v1/project/member/leave` | `project_id` | Removes only the caller's membership. |

`name` is trimmed text of 1–191 bytes without control characters.
`description` is nullable text up to 8000 bytes; whitespace-only text becomes
`null`. Omission on update retains the old description. Listing accepts `q`
up to 191 bytes as a project-name search, `limit` from 1–100 (default 20), and
nonnegative `offset` (default 0). Results contain `items`, `total_count`,
`limit`, `offset`, nullable `next_offset`, and `principal_capabilities`.
Each item contains project fields, the caller's `role`, and `capabilities`.

An admin cannot create, change, or remove owner/admin memberships. Privileged
assignments also require `project.member.assign_roles`. Re-adding a removed
principal reactivates its membership ID with active status and retains its role
unless a new role is supplied; membership history
is preserved. Membership results contain `data.member` with `id`, `principal`,
`role`, `status`, timestamps and optional `identity` (`id`, `name`, `kind`).
The member list additionally supplies `can_edit` per row.

## Create and select a project

```bash
curl -sS https://api.drop.service.m7.org/v1/project/create \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Example handoffs","description":"Short-lived payloads"}'
```

```json
{
  "status": 1,
  "comment": "OK",
  "data": {
    "project": {
      "id": "PROJECT_UUID",
      "name": "Example handoffs",
      "description": "Short-lived payloads"
    },
    "management_group_id": "GROUP_UUID",
    "membership": {"id":"MEMBERSHIP_UUID","principal":"PRINCIPAL_UUID","role":"owner","status":"active"},
    "capabilities": ["project.get", "drop.store", "project.delete"]
  }
}
```

The example abbreviates project fields and capabilities. Project records also
return `created_by`, `updated_by`, `created_at`, and `updated_at`.

## Review and confirm cascade deletion

First call `/v1/project/get` with `project_id` and
`include_deletion_summary: true`. An authorized owner receives
`data.deletion_summary` containing six nonnegative integer counts: `total`,
`active`, `reserved`, `consumed`, `revoked`, and `expired`. Show this summary
to the user and send the exact object back as `summary` with `confirmed: true`:

```json
{
  "project_id": "PROJECT_UUID",
  "confirmed": true,
  "summary": {"total":2,"active":1,"reserved":1,"consumed":0,"revoked":0,"expired":0}
}
```

The service compares the reviewed counts under the project lock before deleting
all drops and retiring the project's associations. A changed summary returns
HTTP `409`; fetch and review a new summary before confirming again. Success
returns `project_id`, `deleted: true`, and `deleted_drops` with the final counts.
Missing confirmation or malformed counts return `400`. Missing projects return
`404`; denied membership/capability returns `403`; a busy project mutation
returns `409` and can be retried after reloading context.
