From 135ac7e0dd2d3cc6d33bdf8410e132af9d1d00ec Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Wed, 2 Sep 2026 19:25:54 -0700 Subject: [PATCH] docs: access control guide and corrected env var descriptions Claude-Session: https://claude.ai/code/session_014aPQ5yqG9QFxKnbwQfCYKa --- .../docs/guides/installing/access-control.md | 80 ++++ .../installing/downloading-streamplace.md | 9 +- .../access/place-stream-access-creategrant.md | 114 +++++ .../access/place-stream-access-defs.md | 175 ++++++++ .../access/place-stream-access-deletegrant.md | 97 +++++ .../access/place-stream-access-getstatus.md | 79 ++++ .../access/place-stream-access-grant.md | 70 +++ .../access/place-stream-access-listgrants.md | 85 ++++ .../access/place-stream-access-policy.md | 60 +++ .../place-stream-access-updatepolicy.md | 101 +++++ .../place-stream-broadcast-getbroadcaster.md | 12 +- .../content/docs/lex-reference/openapi.json | 398 +++++++++++++++++- 12 files changed, 1271 insertions(+), 9 deletions(-) create mode 100644 js/docs/src/content/docs/guides/installing/access-control.md create mode 100644 js/docs/src/content/docs/lex-reference/access/place-stream-access-creategrant.md create mode 100644 js/docs/src/content/docs/lex-reference/access/place-stream-access-defs.md create mode 100644 js/docs/src/content/docs/lex-reference/access/place-stream-access-deletegrant.md create mode 100644 js/docs/src/content/docs/lex-reference/access/place-stream-access-getstatus.md create mode 100644 js/docs/src/content/docs/lex-reference/access/place-stream-access-grant.md create mode 100644 js/docs/src/content/docs/lex-reference/access/place-stream-access-listgrants.md create mode 100644 js/docs/src/content/docs/lex-reference/access/place-stream-access-policy.md create mode 100644 js/docs/src/content/docs/lex-reference/access/place-stream-access-updatepolicy.md diff --git a/js/docs/src/content/docs/guides/installing/access-control.md b/js/docs/src/content/docs/guides/installing/access-control.md new file mode 100644 index 000000000..750aa0e04 --- /dev/null +++ b/js/docs/src/content/docs/guides/installing/access-control.md @@ -0,0 +1,80 @@ +--- +title: Access control +description: Decide who can view, stream to, upload to, and be syndicated by your node. +--- + +Every Streamplace node has a small role-based access policy. Admins edit it +from **Settings → Access** in the app; the environment variables you may +already be using keep working as seeds. + +## Roles + +| Role | What it allows | +| ----------- | ------------------------------------------------------------------------------- | +| `admin` | Manage branding and access control. Admins hold every other role automatically. | +| `viewer` | Use the frontend and playback. Only matters when the viewer mode is not `open`. | +| `streamer` | Ingest live media to this node. | +| `syndicate` | Have this account's media carried from other nodes onto this one. | +| `vod` | Upload videos and have livestreams recorded. | + +## Modes + +Each role has a mode: + +- **open** — everyone holds the role, including anonymous visitors. +- **allowlist** — only accounts with a grant hold the role. +- **off** — nobody holds the role. Admins are always exempt. + +The `admin` role is always `allowlist`. + +## A private node + +To bring up a node that nobody can use until you let them in, start it with +your own DID as admin and the viewer role seeded to `allowlist`: + +```bash +SP_ADMIN_DIDS=did:plc:yourdid SP_ACCESS_POLICY=viewer=allowlist streamplace +``` + +Sign in as that admin, open **Settings → Access**, and grant `viewer` to the +accounts that should get in. Admins never need a viewer grant. + +Set the `viewer` role to `allowlist` and the node answers nothing but the +sign-in flow to anyone who is not on the list. That includes the API, link +cards, playback, thumbnails and chat. Branding stays public so the sign-in +wall shows the node's own logo and name. Visitors see a +"this node is private" wall, and signed-in accounts that are not on the list +see "you're not on the list". Nodes that share a service key with this one +(the same station) are not affected. + +Grant `viewer` to the accounts that should get in. Admins never need a +viewer grant. + +## Environment seeds + +The variables below seed grants at startup. They show up in the Access screen +marked "from environment" and cannot be revoked there; remove them from the +environment instead. Grants you add in the app live in the node's state +database and survive restarts. + +| Variable | Seeds | +| ------------------------ | ------------------------------------------------------------------------------------------------ | +| `SP_ADMIN_DIDS` | `admin` grants. You need at least one to reach the Access screen the first time. | +| `SP_ACCESS_POLICY` | Initial modes as `role=mode` pairs, e.g. `viewer=allowlist,vod=off`. A mode set in the app wins. | +| `SP_ALLOWED_STREAMS` | `streamer` grants. When set, the streamer mode defaults to `allowlist`; when empty, to `open`. | +| `SP_SYNDICATE` | `syndicate` grants. `*` makes the default mode `open`; empty makes it `off`. | +| `SP_WIDE_OPEN` | Forces viewer, streamer and vod to `open`. Development only. | +| `SP_DISABLE_SYNDICATION` | Forces syndicate to `off` in both directions. | +| `SP_BETA_INVITE_DID` | Accounts holding a `place.stream.beta.invite` for `vod` from this DID also hold the `vod` role. | + +When no vod mode has been set explicitly and no invite issuer is configured, +anyone who may stream may also upload, which is what older nodes did. + +## Where the data lives + +Grants and the policy are modeled as records in an atproto space owned by +the node's broadcaster DID, addressed as +`at://{broadcaster}/space/place.stream.access.control/self/{admin}/place.stream.access.grant/{rkey}`. +Until the atproto spaces implementation ships they are stored in the node's +state database under those URIs, so they can move into a real space later +without changing their shape. The API lives under `place.stream.access.*`. diff --git a/js/docs/src/content/docs/guides/installing/downloading-streamplace.md b/js/docs/src/content/docs/guides/installing/downloading-streamplace.md index 412593408..2f99a8769 100644 --- a/js/docs/src/content/docs/guides/installing/downloading-streamplace.md +++ b/js/docs/src/content/docs/guides/installing/downloading-streamplace.md @@ -34,7 +34,9 @@ SP_HTTP_ADDR=:80 SP_HTTPS_ADDR=:443 SP_SECURE=true -# Set this variable to your did:plc or did:web to have admin access to the node +# Set this variable to your did:plc or did:web to have admin access to the node. +# Admins can manage branding and access control from Settings in the app, +# including granting more admins, so you only need to seed the first one here. SP_ADMIN_DIDS=did:web:example.com,did:plc:rbvrr34edl5ddpuwcubjiost # If you're running Streamplace behind an HTTPS proxy, you'll want @@ -46,7 +48,10 @@ SP_BROADCASTER_HOST=example.com # If you have a multi-node cluster, they'll each need different public DNS names: SP_SERVER_HOST=prod-nyc0.example.com -# If you don't want to syndicate everyone, add your list of allowed DIDs here: +# By default anyone with an atproto account can stream to your node. To +# restrict ingest to a list of DIDs, seed the streamer allowlist here. You can +# also manage this (and who may view, upload, or be syndicated) from +# Settings → Access in the app; see the Access control guide. SP_ALLOWED_STREAMS=did:web:example.com,did:plc:rbvrr34edl5ddpuwcubjiost # Useful if your TLS cert and key aren't in the default diff --git a/js/docs/src/content/docs/lex-reference/access/place-stream-access-creategrant.md b/js/docs/src/content/docs/lex-reference/access/place-stream-access-creategrant.md new file mode 100644 index 000000000..8ae402986 --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/access/place-stream-access-creategrant.md @@ -0,0 +1,114 @@ +--- +title: place.stream.access.createGrant +description: Reference for the place.stream.access.createGrant lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `procedure` + +Grant a role to an account. Requires the admin role. Idempotent: granting a role an account already holds returns the existing grant. + +**Parameters:** _(None defined)_ + +**Input:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| --------- | ------------------------------------------------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------- | --------------------------------------- | +| `subject` | `string` | ✅ | The account to grant to, as a DID or a handle. Handles are resolved to DIDs before the grant is written. | | +| `role` | [`place.stream.access.defs#role`](/lex-reference/place-stream-access-defs#role) | ✅ | | | +| `note` | `string` | ❌ | | Max Length: 1000
Max Graphemes: 100 | + +**Output:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| ------- | ----------------------------------------------------------------------------------------- | ----- | ----------- | ----------- | +| `grant` | [`place.stream.access.defs#grantView`](/lex-reference/place-stream-access-defs#grantview) | ✅ | | | + +**Possible Errors:** + +- `Unauthorized`: The caller is not an admin. +- `InvalidSubject`: The subject is neither a valid DID nor a resolvable handle. +- `InvalidRole`: The role is not one this node knows about. + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.access.createGrant", + "defs": { + "main": { + "type": "procedure", + "description": "Grant a role to an account. Requires the admin role. Idempotent: granting a role an account already holds returns the existing grant.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["subject", "role"], + "properties": { + "subject": { + "type": "string", + "description": "The account to grant to, as a DID or a handle. Handles are resolved to DIDs before the grant is written." + }, + "role": { + "type": "ref", + "ref": "place.stream.access.defs#role" + }, + "note": { + "type": "string", + "maxLength": 1000, + "maxGraphemes": 100 + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["grant"], + "properties": { + "grant": { + "type": "ref", + "ref": "place.stream.access.defs#grantView" + } + } + } + }, + "errors": [ + { + "name": "Unauthorized", + "description": "The caller is not an admin." + }, + { + "name": "InvalidSubject", + "description": "The subject is neither a valid DID nor a resolvable handle." + }, + { + "name": "InvalidRole", + "description": "The role is not one this node knows about." + } + ] + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/access/place-stream-access-defs.md b/js/docs/src/content/docs/lex-reference/access/place-stream-access-defs.md new file mode 100644 index 000000000..3007e4090 --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/access/place-stream-access-defs.md @@ -0,0 +1,175 @@ +--- +title: place.stream.access.defs +description: Reference for the place.stream.access.defs lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `role` + +**Type:** `string` + +A capability a node grants to an account. admin: manage branding and access control (implies every other role). viewer: use the frontend and playback when the node is private. streamer: ingest media to this node. syndicate: have this account's media carried from other nodes. vod: upload and record VODs. + +**Constraints:**
Known Values: `admin`, `viewer`, `streamer`, `syndicate`, `vod` + +--- + + + +### `mode` + +**Type:** `string` + +How a node decides a role. open: every account holds the role. allowlist: only accounts with a grant hold the role. off: nobody holds the role (admins are always exempt). + +**Constraints:**
Known Values: `open`, `allowlist`, `off` + +--- + + + +### `roleMode` + +**Type:** `object` + +**Properties:** + +| Name | Type | Req'd | Description | Constraints | +| ------ | ---------------- | ----- | ----------- | ----------- | +| `role` | [`#role`](#role) | ✅ | | | +| `mode` | [`#mode`](#mode) | ✅ | | | + +--- + + + +### `grantView` + +**Type:** `object` + +One account's grant of one role. Grants stored as place.stream.access.grant records in the node's access-control space carry a uri and cid; grants seeded from the node's environment (SP_ADMIN_DIDS, SP_ALLOWED_STREAMS, SP_SYNDICATE) have no uri and cannot be revoked from the API. + +**Properties:** + +| Name | Type | Req'd | Description | Constraints | +| ----------- | ---------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | +| `uri` | `string` | ❌ | at://{authority}/space/place.stream.access.control/self/{author}/place.stream.access.grant/{rkey} (A space URI; not validated as a classic at-uri because the space form is newer than that grammar.) | | +| `cid` | `string` | ❌ | | Format: `cid` | +| `subject` | `string` | ✅ | | Format: `did` | +| `role` | [`#role`](#role) | ✅ | | | +| `source` | `string` | ✅ | space: a record in the access-control space, editable via the API. environment: seeded from the node's configuration. | Known Values: `space`, `environment` | +| `createdBy` | `string` | ❌ | The admin that created the grant (the record's author). | Format: `did` | +| `createdAt` | `string` | ❌ | | Format: `datetime` | +| `note` | `string` | ❌ | | Max Length: 1000
Max Graphemes: 100 | + +--- + + + +### `policyView` + +**Type:** `object` + +**Properties:** + +| Name | Type | Req'd | Description | Constraints | +| ------- | --------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `roles` | Array of [`#roleMode`](#rolemode) | ✅ | The effective mode of every role the node knows about, including environment overrides such as SP_WIDE_OPEN and SP_DISABLE_SYNDICATION. | | + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.access.defs", + "defs": { + "role": { + "type": "string", + "description": "A capability a node grants to an account. admin: manage branding and access control (implies every other role). viewer: use the frontend and playback when the node is private. streamer: ingest media to this node. syndicate: have this account's media carried from other nodes. vod: upload and record VODs.", + "knownValues": ["admin", "viewer", "streamer", "syndicate", "vod"] + }, + "mode": { + "type": "string", + "description": "How a node decides a role. open: every account holds the role. allowlist: only accounts with a grant hold the role. off: nobody holds the role (admins are always exempt).", + "knownValues": ["open", "allowlist", "off"] + }, + "roleMode": { + "type": "object", + "required": ["role", "mode"], + "properties": { + "role": { + "type": "ref", + "ref": "#role" + }, + "mode": { + "type": "ref", + "ref": "#mode" + } + } + }, + "grantView": { + "type": "object", + "description": "One account's grant of one role. Grants stored as place.stream.access.grant records in the node's access-control space carry a uri and cid; grants seeded from the node's environment (SP_ADMIN_DIDS, SP_ALLOWED_STREAMS, SP_SYNDICATE) have no uri and cannot be revoked from the API.", + "required": ["subject", "role", "source"], + "properties": { + "uri": { + "type": "string", + "description": "at://{authority}/space/place.stream.access.control/self/{author}/place.stream.access.grant/{rkey} (A space URI; not validated as a classic at-uri because the space form is newer than that grammar.)" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "subject": { + "type": "string", + "format": "did" + }, + "role": { + "type": "ref", + "ref": "#role" + }, + "source": { + "type": "string", + "knownValues": ["space", "environment"], + "description": "space: a record in the access-control space, editable via the API. environment: seeded from the node's configuration." + }, + "createdBy": { + "type": "string", + "format": "did", + "description": "The admin that created the grant (the record's author)." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "note": { + "type": "string", + "maxLength": 1000, + "maxGraphemes": 100 + } + } + }, + "policyView": { + "type": "object", + "required": ["roles"], + "properties": { + "roles": { + "type": "array", + "description": "The effective mode of every role the node knows about, including environment overrides such as SP_WIDE_OPEN and SP_DISABLE_SYNDICATION.", + "items": { + "type": "ref", + "ref": "#roleMode" + } + } + } + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/access/place-stream-access-deletegrant.md b/js/docs/src/content/docs/lex-reference/access/place-stream-access-deletegrant.md new file mode 100644 index 000000000..d2f7eec2c --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/access/place-stream-access-deletegrant.md @@ -0,0 +1,97 @@ +--- +title: place.stream.access.deleteGrant +description: Reference for the place.stream.access.deleteGrant lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `procedure` + +Revoke a grant by its space URI. Requires the admin role. Grants seeded from the environment have no URI and cannot be revoked here. + +**Parameters:** _(None defined)_ + +**Input:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| ----- | -------- | ----- | --------------------------------------------------------------------------------------------------- | ----------- | +| `uri` | `string` | ✅ | (A space URI; not validated as a classic at-uri because the space form is newer than that grammar.) | | + +**Output:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| --------- | --------- | ----- | ----------- | ----------- | +| `success` | `boolean` | ✅ | | | + +**Possible Errors:** + +- `Unauthorized`: The caller is not an admin. +- `NotFound`: No grant with that URI. + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.access.deleteGrant", + "defs": { + "main": { + "type": "procedure", + "description": "Revoke a grant by its space URI. Requires the admin role. Grants seeded from the environment have no URI and cannot be revoked here.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "description": "(A space URI; not validated as a classic at-uri because the space form is newer than that grammar.)" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["success"], + "properties": { + "success": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "Unauthorized", + "description": "The caller is not an admin." + }, + { + "name": "NotFound", + "description": "No grant with that URI." + } + ] + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/access/place-stream-access-getstatus.md b/js/docs/src/content/docs/lex-reference/access/place-stream-access-getstatus.md new file mode 100644 index 000000000..8b9c235ff --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/access/place-stream-access-getstatus.md @@ -0,0 +1,79 @@ +--- +title: place.stream.access.getStatus +description: Reference for the place.stream.access.getStatus lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `query` + +Report the caller's roles on this node and the node's access policy. Works unauthenticated (roles then reflect what an anonymous visitor holds). This is the one place.stream method a node always answers, even to accounts locked out by a private viewer policy, so clients can render the right wall. + +**Parameters:** _(None defined)_ + +**Output:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| -------- | ------------------------------------------------------------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `did` | `string` | ❌ | The authenticated caller, when there is one. | Format: `did` | +| `roles` | Array of [`place.stream.access.defs#role`](/lex-reference/place-stream-access-defs#role) | ✅ | Every role the caller effectively holds. | | +| `policy` | [`place.stream.access.defs#policyView`](/lex-reference/place-stream-access-defs#policyview) | ✅ | | | +| `space` | `string` | ✅ | The node's access-control space: at://{authority}/space/place.stream.access.control/self (A space URI; not validated as a classic at-uri because the space form is newer than that grammar.) | | + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.access.getStatus", + "defs": { + "main": { + "type": "query", + "description": "Report the caller's roles on this node and the node's access policy. Works unauthenticated (roles then reflect what an anonymous visitor holds). This is the one place.stream method a node always answers, even to accounts locked out by a private viewer policy, so clients can render the right wall.", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["roles", "policy", "space"], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "The authenticated caller, when there is one." + }, + "roles": { + "type": "array", + "description": "Every role the caller effectively holds.", + "items": { + "type": "ref", + "ref": "place.stream.access.defs#role" + } + }, + "policy": { + "type": "ref", + "ref": "place.stream.access.defs#policyView" + }, + "space": { + "type": "string", + "description": "The node's access-control space: at://{authority}/space/place.stream.access.control/self (A space URI; not validated as a classic at-uri because the space form is newer than that grammar.)" + } + } + } + } + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/access/place-stream-access-grant.md b/js/docs/src/content/docs/lex-reference/access/place-stream-access-grant.md new file mode 100644 index 000000000..50038b735 --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/access/place-stream-access-grant.md @@ -0,0 +1,70 @@ +--- +title: place.stream.access.grant +description: Reference for the place.stream.access.grant lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `record` + +Grants one role to one account on a node. Lives in the node's access-control space (space type place.stream.access.control, skey self, authority = the broadcaster DID) and is authored by the admin who created it; until the atproto spaces implementation ships, the node stores it in statedb addressed by its at:// space URI. + +**Record Key:** `tid` + +**Record Properties:** + +| Name | Type | Req'd | Description | Constraints | +| ----------- | ------------------------------------------------------------------------------- | ----- | ------------------------------------------- | --------------------------------------- | +| `subject` | `string` | ✅ | The account receiving the role. | Format: `did` | +| `role` | [`place.stream.access.defs#role`](/lex-reference/place-stream-access-defs#role) | ✅ | | | +| `createdAt` | `string` | ✅ | | Format: `datetime` | +| `note` | `string` | ❌ | Free-form reminder of why the grant exists. | Max Length: 1000
Max Graphemes: 100 | + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.access.grant", + "defs": { + "main": { + "type": "record", + "key": "tid", + "description": "Grants one role to one account on a node. Lives in the node's access-control space (space type place.stream.access.control, skey self, authority = the broadcaster DID) and is authored by the admin who created it; until the atproto spaces implementation ships, the node stores it in statedb addressed by its at:// space URI.", + "record": { + "type": "object", + "required": ["subject", "role", "createdAt"], + "properties": { + "subject": { + "type": "string", + "format": "did", + "description": "The account receiving the role." + }, + "role": { + "type": "ref", + "ref": "place.stream.access.defs#role" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "note": { + "type": "string", + "maxLength": 1000, + "maxGraphemes": 100, + "description": "Free-form reminder of why the grant exists." + } + } + } + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/access/place-stream-access-listgrants.md b/js/docs/src/content/docs/lex-reference/access/place-stream-access-listgrants.md new file mode 100644 index 000000000..9fc2db546 --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/access/place-stream-access-listgrants.md @@ -0,0 +1,85 @@ +--- +title: place.stream.access.listGrants +description: Reference for the place.stream.access.listGrants lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `query` + +List every grant on this node, including grants seeded from the environment. Requires the admin role. + +**Parameters:** + +| Name | Type | Req'd | Description | Constraints | +| ------ | -------- | ----- | -------------------------------- | ----------- | +| `role` | `string` | ❌ | Only return grants of this role. | | + +**Output:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| -------- | -------------------------------------------------------------------------------------------------- | ----- | ----------- | ----------- | +| `grants` | Array of [`place.stream.access.defs#grantView`](/lex-reference/place-stream-access-defs#grantview) | ✅ | | | + +**Possible Errors:** + +- `Unauthorized`: The caller is not an admin. + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.access.listGrants", + "defs": { + "main": { + "type": "query", + "description": "List every grant on this node, including grants seeded from the environment. Requires the admin role.", + "parameters": { + "type": "params", + "properties": { + "role": { + "type": "string", + "description": "Only return grants of this role." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["grants"], + "properties": { + "grants": { + "type": "array", + "items": { + "type": "ref", + "ref": "place.stream.access.defs#grantView" + } + } + } + } + }, + "errors": [ + { + "name": "Unauthorized", + "description": "The caller is not an admin." + } + ] + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/access/place-stream-access-policy.md b/js/docs/src/content/docs/lex-reference/access/place-stream-access-policy.md new file mode 100644 index 000000000..ad85fabc1 --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/access/place-stream-access-policy.md @@ -0,0 +1,60 @@ +--- +title: place.stream.access.policy +description: Reference for the place.stream.access.policy lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `record` + +The node's access policy: the mode of each role. A single record authored by the space authority in the node's access-control space. Roles absent from the record use the node's defaults. + +**Record Key:** `literal:self` + +**Record Properties:** + +| Name | Type | Req'd | Description | Constraints | +| ----------- | ------------------------------------------------------------------------------------------------ | ----- | ----------- | ------------------ | +| `roles` | Array of [`place.stream.access.defs#roleMode`](/lex-reference/place-stream-access-defs#rolemode) | ✅ | | | +| `updatedAt` | `string` | ✅ | | Format: `datetime` | + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.access.policy", + "defs": { + "main": { + "type": "record", + "key": "literal:self", + "description": "The node's access policy: the mode of each role. A single record authored by the space authority in the node's access-control space. Roles absent from the record use the node's defaults.", + "record": { + "type": "object", + "required": ["roles", "updatedAt"], + "properties": { + "roles": { + "type": "array", + "items": { + "type": "ref", + "ref": "place.stream.access.defs#roleMode" + } + }, + "updatedAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/access/place-stream-access-updatepolicy.md b/js/docs/src/content/docs/lex-reference/access/place-stream-access-updatepolicy.md new file mode 100644 index 000000000..cea30b71d --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/access/place-stream-access-updatepolicy.md @@ -0,0 +1,101 @@ +--- +title: place.stream.access.updatePolicy +description: Reference for the place.stream.access.updatePolicy lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `procedure` + +Set the mode of one or more roles. Requires the admin role. Roles not mentioned keep their current mode. The admin role cannot be changed: it is always allowlist. + +**Parameters:** _(None defined)_ + +**Input:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| ------- | ------------------------------------------------------------------------------------------------ | ----- | ----------- | ----------- | +| `roles` | Array of [`place.stream.access.defs#roleMode`](/lex-reference/place-stream-access-defs#rolemode) | ✅ | | | + +**Output:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| -------- | ------------------------------------------------------------------------------------------- | ----- | ----------- | ----------- | +| `policy` | [`place.stream.access.defs#policyView`](/lex-reference/place-stream-access-defs#policyview) | ✅ | | | + +**Possible Errors:** + +- `Unauthorized`: The caller is not an admin. +- `InvalidRole`: A role or mode is not one this node knows about. + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.access.updatePolicy", + "defs": { + "main": { + "type": "procedure", + "description": "Set the mode of one or more roles. Requires the admin role. Roles not mentioned keep their current mode. The admin role cannot be changed: it is always allowlist.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["roles"], + "properties": { + "roles": { + "type": "array", + "items": { + "type": "ref", + "ref": "place.stream.access.defs#roleMode" + } + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["policy"], + "properties": { + "policy": { + "type": "ref", + "ref": "place.stream.access.defs#policyView" + } + } + } + }, + "errors": [ + { + "name": "Unauthorized", + "description": "The caller is not an admin." + }, + { + "name": "InvalidRole", + "description": "A role or mode is not one this node knows about." + } + ] + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/broadcast/place-stream-broadcast-getbroadcaster.md b/js/docs/src/content/docs/lex-reference/broadcast/place-stream-broadcast-getbroadcaster.md index 99c060202..1388cf013 100644 --- a/js/docs/src/content/docs/lex-reference/broadcast/place-stream-broadcast-getbroadcaster.md +++ b/js/docs/src/content/docs/lex-reference/broadcast/place-stream-broadcast-getbroadcaster.md @@ -24,11 +24,11 @@ Get information about a Streamplace broadcaster. **Schema Type:** `object` -| Name | Type | Req'd | Description | Constraints | -| ------------- | ----------------- | ----- | --------------------------------------------------------------- | ------------- | -| `broadcaster` | `string` | ✅ | DID of the Streamplace broadcaster to which this server belongs | Format: `did` | -| `server` | `string` | ❌ | DID of this particular Streamplace server | Format: `did` | -| `admins` | Array of `string` | ❌ | Array of DIDs authorized as admins | | +| Name | Type | Req'd | Description | Constraints | +| ------------- | ----------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | +| `broadcaster` | `string` | ✅ | DID of the Streamplace broadcaster to which this server belongs | Format: `did` | +| `server` | `string` | ❌ | DID of this particular Streamplace server | Format: `did` | +| `admins` | Array of `string` | ❌ | Deprecated and no longer populated. Clients learn their own roles from place.stream.access.getStatus; admins manage grants via place.stream.access.\*. | | --- @@ -69,7 +69,7 @@ Get information about a Streamplace broadcaster. "type": "string", "format": "did" }, - "description": "Array of DIDs authorized as admins" + "description": "Deprecated and no longer populated. Clients learn their own roles from place.stream.access.getStatus; admins manage grants via place.stream.access.*." } } } diff --git a/js/docs/src/content/docs/lex-reference/openapi.json b/js/docs/src/content/docs/lex-reference/openapi.json index 09e35d024..32ec1d806 100644 --- a/js/docs/src/content/docs/lex-reference/openapi.json +++ b/js/docs/src/content/docs/lex-reference/openapi.json @@ -3824,7 +3824,7 @@ }, "admins": { "type": "array", - "description": "Array of DIDs authorized as admins", + "description": "Deprecated and no longer populated. Clients learn their own roles from place.stream.access.getStatus; admins manage grants via place.stream.access.*.", "items": { "type": "string", "format": "did" @@ -4279,6 +4279,330 @@ ] } }, + "/xrpc/place.stream.access.createGrant": { + "post": { + "summary": "Grant a role to an account. Requires the admin role. Idempotent: granting a role an account already holds returns the existing grant.", + "operationId": "place.stream.access.createGrant", + "tags": ["place.stream.access"], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "grant": { + "$ref": "#/components/schemas/place.stream.access.defs_grantView" + } + }, + "required": ["grant"] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["error", "message"], + "properties": { + "error": { + "type": "string", + "oneOf": [ + { + "const": "Unauthorized" + }, + { + "const": "InvalidSubject" + }, + { + "const": "InvalidRole" + } + ] + }, + "message": { + "type": "string" + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "subject": { + "type": "string", + "description": "The account to grant to, as a DID or a handle. Handles are resolved to DIDs before the grant is written." + }, + "role": { + "$ref": "#/components/schemas/place.stream.access.defs_role" + }, + "note": { + "type": "string", + "maxLength": 1000 + } + }, + "required": ["subject", "role"] + } + } + } + } + } + }, + "/xrpc/place.stream.access.deleteGrant": { + "post": { + "summary": "Revoke a grant by its space URI. Requires the admin role. Grants seeded from the environment have no URI and cannot be revoked here.", + "operationId": "place.stream.access.deleteGrant", + "tags": ["place.stream.access"], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["error", "message"], + "properties": { + "error": { + "type": "string", + "oneOf": [ + { + "const": "Unauthorized" + }, + { + "const": "NotFound" + } + ] + }, + "message": { + "type": "string" + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "(A space URI; not validated as a classic at-uri because the space form is newer than that grammar.)" + } + }, + "required": ["uri"] + } + } + } + } + } + }, + "/xrpc/place.stream.access.getStatus": { + "get": { + "summary": "Report the caller's roles on this node and the node's access policy. Works unauthenticated (roles then reflect what an anonymous visitor holds). This is the one place.stream method a node always answers, even to accounts locked out by a private viewer policy, so clients can render the right wall.", + "operationId": "place.stream.access.getStatus", + "tags": ["place.stream.access"], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "did": { + "type": "string", + "description": "The authenticated caller, when there is one.", + "format": "did" + }, + "roles": { + "type": "array", + "description": "Every role the caller effectively holds.", + "items": { + "$ref": "#/components/schemas/place.stream.access.defs_role" + } + }, + "policy": { + "$ref": "#/components/schemas/place.stream.access.defs_policyView" + }, + "space": { + "type": "string", + "description": "The node's access-control space: at://{authority}/space/place.stream.access.control/self (A space URI; not validated as a classic at-uri because the space form is newer than that grammar.)" + } + }, + "required": ["roles", "policy", "space"] + } + } + } + } + } + } + }, + "/xrpc/place.stream.access.listGrants": { + "get": { + "summary": "List every grant on this node, including grants seeded from the environment. Requires the admin role.", + "operationId": "place.stream.access.listGrants", + "tags": ["place.stream.access"], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "grants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/place.stream.access.defs_grantView" + } + } + }, + "required": ["grants"] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["error", "message"], + "properties": { + "error": { + "type": "string", + "oneOf": [ + { + "const": "Unauthorized" + } + ] + }, + "message": { + "type": "string" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "role", + "in": "query", + "required": false, + "description": "Only return grants of this role.", + "schema": { + "type": "string", + "description": "Only return grants of this role." + } + } + ] + } + }, + "/xrpc/place.stream.access.updatePolicy": { + "post": { + "summary": "Set the mode of one or more roles. Requires the admin role. Roles not mentioned keep their current mode. The admin role cannot be changed: it is always allowlist.", + "operationId": "place.stream.access.updatePolicy", + "tags": ["place.stream.access"], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "policy": { + "$ref": "#/components/schemas/place.stream.access.defs_policyView" + } + }, + "required": ["policy"] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["error", "message"], + "properties": { + "error": { + "type": "string", + "oneOf": [ + { + "const": "Unauthorized" + }, + { + "const": "InvalidRole" + } + ] + }, + "message": { + "type": "string" + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/place.stream.access.defs_roleMode" + } + } + }, + "required": ["roles"] + } + } + } + } + } + }, "/xrpc/games.gamesgamesgamesgames.search": { "get": { "summary": "Search across all entity types (games, profiles, platforms, collections, engines).", @@ -7370,6 +7694,78 @@ }, "required": ["issuanceUri", "badgeType", "issuer"] }, + "place.stream.access.defs_role": { + "type": "string", + "description": "A capability a node grants to an account. admin: manage branding and access control (implies every other role). viewer: use the frontend and playback when the node is private. streamer: ingest media to this node. syndicate: have this account's media carried from other nodes. vod: upload and record VODs." + }, + "place.stream.access.defs_grantView": { + "type": "object", + "description": "One account's grant of one role. Grants stored as place.stream.access.grant records in the node's access-control space carry a uri and cid; grants seeded from the node's environment (SP_ADMIN_DIDS, SP_ALLOWED_STREAMS, SP_SYNDICATE) have no uri and cannot be revoked from the API.", + "properties": { + "uri": { + "type": "string", + "description": "at://{authority}/space/place.stream.access.control/self/{author}/place.stream.access.grant/{rkey} (A space URI; not validated as a classic at-uri because the space form is newer than that grammar.)" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "subject": { + "type": "string", + "format": "did" + }, + "role": { + "$ref": "#/components/schemas/place.stream.access.defs_role" + }, + "source": { + "type": "string", + "description": "space: a record in the access-control space, editable via the API. environment: seeded from the node's configuration." + }, + "createdBy": { + "type": "string", + "description": "The admin that created the grant (the record's author).", + "format": "did" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "note": { + "type": "string", + "maxLength": 1000 + } + }, + "required": ["subject", "role", "source"] + }, + "place.stream.access.defs_policyView": { + "type": "object", + "properties": { + "roles": { + "type": "array", + "description": "The effective mode of every role the node knows about, including environment overrides such as SP_WIDE_OPEN and SP_DISABLE_SYNDICATION.", + "items": { + "$ref": "#/components/schemas/place.stream.access.defs_roleMode" + } + } + }, + "required": ["roles"] + }, + "place.stream.access.defs_roleMode": { + "type": "object", + "properties": { + "role": { + "$ref": "#/components/schemas/place.stream.access.defs_role" + }, + "mode": { + "$ref": "#/components/schemas/place.stream.access.defs_mode" + } + }, + "required": ["role", "mode"] + }, + "place.stream.access.defs_mode": { + "type": "string", + "description": "How a node decides a role. open: every account holds the role. allowlist: only accounts with a grant hold the role. off: nobody holds the role (admins are always exempt)." + }, "com.atproto.sync.listRepos_repo": { "type": "object", "properties": { -- 2.51.2