# Publication membership and capabilities

Manage collaborators in a publication's Management membership. Membership is
separate from public author profiles and from service-wide admission.

## Authentication

Every route requires a bearer token and active publication membership for the
effective Personal or organization principal. Use current
[context](https://m7.org/docs/api/api.blog.service.m7.org/context-route.md) and member action flags before offering an edit.
Owner/Admin roles grant protected management actions, while optional actions
come from current role defaults or a custom capability selection. Organization
grants restrict both sets of actions.

## Routes

Paths below include the `/v1` API prefix. Write routes use `publication_id`;
list routes also accept `publication_slug`.

| Route | Capability | Required fields | Optional fields / result |
| --- | --- | --- | --- |
| `GET` or `POST /v1/member/list` | `member.list` | Publication selector | `limit` (1–100, default 50), `offset` (0–1,000,000, default 0); returns `members`, `total_count`, `next_offset`, group, roles, and active Owner count. |
| `POST /v1/member/get` | `member.list`, or self with `publication.get` | One publication selector; `member_id` or `principal_id` | See [member detail](https://m7.org/docs/api/api.blog.service.m7.org/member-get-route.md). |
| `POST /v1/member/save` | `member.save`; role changes additionally require `member.assign_roles` | `publication_id`, `principal_id` | `member_id` for an existing membership; `role`, `status`; returns `saved`, `member`, and nullable updated `context`. |
| `POST /v1/member/remove` | `member.remove` | `publication_id`, `principal_id`, exact `member_id` | Returns `removed`, member identifiers, and nullable updated `context`. |
| `POST /v1/member/leave` | `member.leave` | `publication_id` | Removes the current workspace's membership; returns `left`. Target principal, member, role, and status fields are rejected. |
| `GET` or `POST /v1/member/capabilities/list` | `member.list` | Publication selector | Returns read-only `tags` and `roles`. |
| `POST /v1/member/capabilities/save` | `member.save` | `publication_id`, `principal_id`, exact `member_id`, `mode`, `tag_ids` | Returns `saved` and updated `context`. |

Membership writes use local Blog roster UUIDs, not native user or organization
IDs. Targets must be active admitted roster principals. Owner can manage all
membership levels; Admin can manage ordinary Members. Accepted roles are
`owner`, `admin`, and `member`; editable statuses are `active` and `disabled`.
For an existing membership, omitted role/status retain current values. A new
direct membership defaults to active Member. An existing membership must be
identified exactly before lifecycle changes; do not use creation as reactivation.

```json
{
  "publication_id": "PUBLICATION_UUID",
  "principal_id": "COLLABORATOR_PRINCIPAL_UUID",
  "member_id": "MEMBERSHIP_UUID",
  "status": "disabled"
}
```

The final active Owner cannot be disabled, demoted, removed, or leave.
Operations recheck authority while applying the change. A successful self-change
can return `context: null` when the caller loses access; stop using the old
context. Inviting or applying uses a separate recipient-consent workflow; see
[invitations and applications](https://m7.org/docs/api/api.blog.service.m7.org/relationship-access.md).

## Select optional capabilities

Load the catalogue for the publication. Each tag has `tag_id`, `capability`,
`name`, and `description`; use `tag_id` in the save request. `roles` describes
current defaults and permitted selections. These are read-only definitions.

```json
{
  "publication_id": "PUBLICATION_UUID",
  "principal_id": "COLLABORATOR_PRINCIPAL_UUID",
  "member_id": "MEMBERSHIP_UUID",
  "mode": "custom",
  "tag_ids": ["CAPABILITY_TAG_UUID"]
}
```

`mode` is `defaults` or `custom`. Send `tag_ids: []` for `defaults`; for `custom`,
an empty list deliberately selects no optional grants. IDs must be unique and
allowed for the target's current role. Changing a role clears its old custom
selection. Protected management rights and the last-owner rule are independent
of optional selections. Member results include `capabilities_mode`, selected
`capability_tag_ids`, `can_update`, `can_remove`, `can_leave`,
`can_edit_capabilities`, `allowed_roles`, and `allowed_statuses`.

## Public capability discovery

`GET /v1/capabilities.json` returns the current read-only service capability
catalogue without authentication. It is a raw JSON document, outside the usual
response envelope, with `schema_version`, `service`, `catalogue_version`,
`families`, `capabilities`, `presets`, and `role_defaults`. Use capability `id`
values to understand action names and profile expansions. Catalogue definitions
do not grant the caller those actions; the selected resource context remains
the access decision. Blog also accepts `HEAD`. Optional positive integer
`catalogue_version` selects a retained catalogue revision; malformed values
return `400`. An unavailable catalogue returns `503`.

## Errors and recovery

Malformed input or invalid/duplicate capability selections return `400`;
missing active membership or permission returns `403`; missing target membership
returns `404`; conflicting lifecycle or last-owner changes return `409`.
Capability catalogue unavailability fails closed.

Remove and Leave coordinate with the relationship for the selected publication
when one exists. A remote failure does not report success or delete the local
membership. Retry the same action after recovery; a lost successful remote reply
can be completed without duplicating the removal event. Direct memberships with
no relationship still support local removal. See the
[relationship recovery contract](https://m7.org/docs/api/api.blog.service.m7.org/relationship-access.md#recovery-and-access-authority).
