[READ-ONLY] Mirror of https://github.com/flo-bit/contrail. atproto backend in a bottle flo-bit.dev/contrail/
0

Configure Feed

Select the types of activity you want to include in your feed.

commit

Florian (Apr 24, 2026, 5:05 PM +0200) 3ee5ed4a f8fa6721

+473 -5099
+6 -5
README.md
··· 83 83 GET https://<your-worker>.workers.dev/xrpc/com.example.event.listRecords?startsAtMin=2026-01-01&limit=10 84 84 ``` 85 85 86 - returns every `community.lexicon.calendar.event` record published anywhere on atproto that matches, as JSON. that's it — no PDS setup, no lexicon publishing, no relay configuration. everything scales from there: add filters, add full-text search, add more collections, turn on [spaces](./docs/04-spaces.md) for private records, mount the handler in sveltekit instead, swap the adapter for postgres. 86 + returns every `community.lexicon.calendar.event` record published anywhere on atproto that matches, as JSON. that's it — no PDS setup, no lexicon publishing, no relay configuration. everything scales from there: add filters, add full-text search, add more collections, turn on [spaces](./docs/05-spaces.md) for private records, mount the handler in sveltekit instead, swap the adapter for postgres. 87 87 88 88 **not using workers?** same library, different `db`. see [adapters](./docs/01-indexing.md#adapters) for node:sqlite and postgres. 89 89 ··· 92 92 - [Indexing](./docs/01-indexing.md) — the core: collections, ingestion, adapters 93 93 - [Querying](./docs/02-querying.md) — filters, sorts, hydration, search, pagination 94 94 - [Lexicons](./docs/03-lexicons.md) — `contrail-lex` CLI, codegen, publishing 95 - - [Spaces](./docs/04-spaces.md) — permissioned records stored by the appview 96 - - [Communities](./docs/05-communities.md) — group-controlled atproto DIDs 97 - - [Sync](./docs/06-sync.md) — reactive client-side store over `watchRecords` 98 - - [Examples](./docs/07-examples.md) — reference deployments in the repo 95 + - [Auth](./docs/04-auth.md) — service-auth JWTs, invite tokens, watch tickets, OAuth permission sets 96 + - [Spaces](./docs/05-spaces.md) — permissioned records stored by the appview 97 + - [Communities](./docs/06-communities.md) — group-controlled atproto DIDs 98 + - [Sync](./docs/07-sync.md) — reactive client-side store over `watchRecords` 99 + - [Examples](./docs/08-examples.md) — reference deployments in the repo 99 100 - Frameworks: [SvelteKit + Cloudflare](./docs/frameworks/sveltekit-cloudflare.md) 100 101 101 102 ## Packages
+11
.changeset/spaces-owner-no-delete-bypass.md
··· 1 + --- 2 + "@atmo-dev/contrail": patch 3 + --- 4 + 5 + tighten spaces ACL: owners no longer bypass the "own-record" rule on delete. everyone in the member list — owner included — can only delete records they authored. 6 + 7 + before: owner calling `space.deleteRecord` on someone else's record returned `200 { ok: true }` (ACL passed, but the adapter's SQL was already scoped to `did = caller`, so no rows were actually deleted — the response lied). 8 + 9 + after: that same call returns `403 { error: "Forbidden", reason: "not-own-record" }`. honest response; no behavior change at the storage layer. 10 + 11 + to wipe someone else's records in a space you own, delete the space itself.
+3 -3
docs/01-indexing.md
··· 198 198 | `jetstreams` | Bluesky | Jetstream URLs | 199 199 | `relays` | Bluesky | Relay URLs for discovery | 200 200 | `notify` | off | `true` opens `notifyOfUpdate`; a string requires `Bearer` | 201 - | `spaces` | — | See [Spaces](./04-spaces.md) | 202 - | `community` | — | See [Communities](./05-communities.md) | 203 - | `realtime` | — | See [Sync](./06-sync.md) | 201 + | `spaces` | — | See [Spaces](./05-spaces.md) | 202 + | `community` | — | See [Communities](./06-communities.md) | 203 + | `realtime` | — | See [Sync](./07-sync.md) |
+139
docs/04-auth.md
··· 1 + # Auth 2 + 3 + Contrail has four auth mechanisms. Which one applies depends on who's calling and what they're asking for. 4 + 5 + | Mechanism | Used by | For | 6 + |---|---|---| 7 + | Anonymous | anyone | public reads | 8 + | Service-auth JWT | third-party apps acting on behalf of a user | anything permissioned (spaces, communities) | 9 + | In-process server client | your own server code | loaders / actions that skip HTTP entirely | 10 + | Invite token | anonymous bearers | read-only access to a specific space | 11 + | Watch ticket | browsers | realtime subscriptions (`watchRecords`) | 12 + 13 + ## Service-auth JWTs 14 + 15 + The standard atproto mechanism. When a third-party app wants to call your contrail service as a user, it: 16 + 17 + 1. Asks the user's PDS to mint a service-auth JWT with `com.atproto.server.getServiceAuth` — the token's claims include `iss` (user's DID), `aud` (your service DID), `lxm` (the specific method NSID), and a short expiry. 18 + 2. Sends the request to your service with `Authorization: Bearer <jwt>` and `Atproto-Proxy: <did>#<service-id>` so the PDS knows where to route. 19 + 20 + Contrail verifies every request against the public key in the issuer's DID doc (`@atcute/xrpc-server` does the heavy lifting). It checks: 21 + 22 + - Signature valid 23 + - `aud` matches the `serviceDid` you configured 24 + - `lxm` covers the method being called 25 + - Token hasn't expired 26 + 27 + On pass, your handler sees a populated `serviceAuth = { issuer, audience, lxm, clientId }` context and can proceed. On fail, 401 or 403 with a structured reason. 28 + 29 + ### The `serviceDid` gotcha 30 + 31 + Use the **plain DID** (no `#fragment`) when configuring contrail: 32 + 33 + ```ts 34 + spaces: { serviceDid: "did:web:example.com" } // right 35 + spaces: { serviceDid: "did:web:example.com#com_example_space" } // wrong 36 + ``` 37 + 38 + Many PDS implementations reject `aud` values containing `#fragment` in `com.atproto.server.getServiceAuth`, and contrail does strict string equality on `aud`. The fragment form belongs only in your DID doc's `service` entry, where PDSes use it to resolve the service endpoint URL for `Atproto-Proxy` routing — that's separate from JWT audience validation. 39 + 40 + ## In-process server client 41 + 42 + When your own server code wants to call contrail, the service-auth dance is pointless — it's your code talking to your code. `createServerClient` skips it: 43 + 44 + ```ts 45 + import { createServerClient } from "@atmo-dev/contrail/server"; 46 + 47 + const client = createServerClient(async (req) => handle(req, env.DB), userDid); 48 + 49 + // Calls bypass fetch entirely; acts as `userDid` for ACL purposes. 50 + const res = await client.get("com.example.event.listRecords", { params: {...} }); 51 + ``` 52 + 53 + Pass `did` to act as that user; omit it for anonymous calls against public endpoints. This is a trust boundary — anything that actually crosses a network needs a real service-auth JWT, not this shortcut. 54 + 55 + See [SvelteKit + Cloudflare](./frameworks/sveltekit-cloudflare.md) for the typical loader pattern. 56 + 57 + ## Invite tokens 58 + 59 + First-class auth for spaces. When a space owner creates an invite: 60 + 61 + ``` 62 + com.example.space.invite.create { spaceUri, ttl?, maxUses? } 63 + → { token: "...plaintext..." } // returned once, never again 64 + ``` 65 + 66 + The plaintext token is handed to the user out-of-band (link, QR, email). Contrail stores only a SHA-256 hash. Redemption is one atomic UPDATE: `used_count++ WHERE hash = ? AND !revoked AND !expired AND !exhausted`. 67 + 68 + Three invite kinds, depending on what the token does: 69 + 70 + - **`join`** — redeemed via `com.example.space.invite.redeem` with a service-auth JWT. Adds the caller's DID to the member list. Members have full read + write inside the space; there's no per-member permission axis beyond "is a member." 71 + - **`read`** — bearer-only. The token itself grants read access when passed as `?inviteToken=<plaintext>`, no DID, no redemption. Good for sharing a read-only link that doesn't add anyone to the member list. 72 + - **`read-join`** — both. Works anonymously as a read token; can also be redeemed with a JWT to promote the caller to member. 73 + 74 + Tokens can be revoked (`invite.revoke`), expire automatically (`ttl`), and be exhausted (`maxUses`). 75 + 76 + ## Watch tickets 77 + 78 + Realtime subscriptions (`watchRecords`) can't use regular service-auth JWTs for two reasons: the WebSocket upgrade can't carry arbitrary headers, and an open socket would outlive a 60s JWT TTL. So contrail uses separate short-lived tickets. 79 + 80 + Server-side minting: 81 + 82 + ``` 83 + com.example.realtime.ticket { spaceUri } 84 + → { ticket: "...", expiresAt: 1234567890 } 85 + ``` 86 + 87 + The ticket is signed by `realtime.ticketSecret` (a 32-byte random, configured once), bound to `(did, spaceUri, querySpec)`. Client uses it on the SSE or WS handshake via `?ticket=...`. 88 + 89 + In the `@atmo-dev/contrail-sync` client: 90 + 91 + ```ts 92 + createWatchStore({ 93 + url: "/xrpc/com.example.message.watchRecords?spaceUri=at://...", 94 + mintTicket: async () => (await fetch("/api/ticket")).then((r) => r.text()), 95 + }); 96 + ``` 97 + 98 + Each reconnect mints a fresh ticket, so expiry doesn't matter for long-lived subscriptions. See [Sync](./07-sync.md) for the full flow. 99 + 100 + ## OAuth permission sets 101 + 102 + When a user grants a third-party app permission to act as them in your contrail service, the consent screen is driven by a **permission set** — a lexicon that bundles every XRPC method you expose. Contrail auto-generates `{namespace}.permissionSet` for you; `contrail-lex` publishes it alongside the other lexicons. 103 + 104 + A third-party app requests scope by referencing your permission set's NSID in its OAuth metadata: 105 + 106 + ```jsonc 107 + "scope": "include:com.example.permissionSet" 108 + ``` 109 + 110 + The user's PDS fetches that lexicon (via DNS-backed NSID resolution), shows the user what methods are being requested, and mints scoped service-auth JWTs on confirmation. 111 + 112 + ### DNS requirements 113 + 114 + Permission sets live under *your* namespace (`com.example.permissionSet`), so PDSes resolve the NSID via DNS. Resolution does **not** walk up subdomains — every authority in your NSID tree needs its own `TXT` record at `_lexicon.<reversed-domain-path>`. 115 + 116 + `contrail-lex publish` prints the exact records you need (also works as a `--dry-run`). Without them, permission sets can't be fetched, and users get errors instead of a consent screen. 117 + 118 + ## Anonymous / public 119 + 120 + No auth needed for: 121 + 122 + - `listRecords` / `getRecord` without `spaceUri` — returns public records 123 + - `getProfile`, `getCursor`, `getOverview` 124 + - `notifyOfUpdate` (unless you set `notify: "some-bearer-token"` in config; then it needs `Authorization: Bearer <that>`) 125 + 126 + Public requests skip all verification middleware — no JWT parsing, no DID-doc fetch. Fast path. 127 + 128 + ## How the pieces fit 129 + 130 + A typical flow for a third-party app acting as a user in a space: 131 + 132 + 1. App registers OAuth client pointing at your permission set NSID. 133 + 2. User grants consent — PDS fetches your permission set lexicon via DNS, shows the user the methods, records the scope. 134 + 3. App calls `com.atproto.server.getServiceAuth` on the user's PDS: `{ aud: "did:web:example.com", lxm: "com.example.space.putRecord", exp: <60s> }`. PDS signs, returns JWT. 135 + 4. App sends `PUT /xrpc/com.example.space.putRecord` with `Authorization: Bearer <jwt>` and `Atproto-Proxy: did:web:example.com#com_example_space`. 136 + 5. User's PDS reads the `Atproto-Proxy` header, resolves the service endpoint from your DID doc, forwards the request. 137 + 6. Contrail verifies the JWT (signature, `aud`, `lxm`, expiry), runs ACL check (is this DID a member of that space?), dispatches the write. 138 + 139 + For your own loaders/actions, steps 3–6 collapse into a single `createServerClient({did}).post(...)` call. For a browser subscribing to a feed, steps 3–5 are replaced by a ticket mint from your server. Same auth model, different surface.
-70
docs/04-spaces.md
··· 1 - # Spaces 2 - 3 - Auth-gated store for records that can't live on public PDSes — private events, invite-only groups, members-only chat. Opt-in; zero cost if you don't enable it. 4 - 5 - ## Mental model 6 - 7 - > A **space** is a bag of records with one lock. The **member list** says who has the key. 8 - 9 - - One owner (DID), one type (NSID), one key. Identified by `at://<owner>/<type>/<key>`. 10 - - Members have `read` or `write`. Owner is implicit `write`. 11 - - Optional **app policy** gates which OAuth clients can act in the space. 12 - 13 - Every permission boundary is its own space. No nested ACLs. Richer roles = more spaces or app-layer checks. 14 - 15 - ## Enable 16 - 17 - ```ts 18 - import type { ContrailConfig } from "@atmo-dev/contrail"; 19 - 20 - const config: ContrailConfig = { 21 - namespace: "com.example", 22 - collections: { /* ... */ }, 23 - spaces: { 24 - type: "com.example.event.space", 25 - serviceDid: "did:web:example.com", 26 - }, 27 - }; 28 - ``` 29 - 30 - Each collection gets a parallel `spaces_records_<short>` table. Opt out per-collection: 31 - 32 - ```ts 33 - public_only: { collection: "com.example.public", allowInSpaces: false } 34 - ``` 35 - 36 - ## Auth 37 - 38 - atproto service-auth JWTs via `@atcute/xrpc-server`. Middleware validates signature, `aud`, and `lxm` before the handler runs. Apps acting in a space mint a JWT against the user's PDS with `Atproto-Proxy: did:web:example.com#com_example_space`, forward to your service, it verifies and executes. 39 - 40 - **Note:** Use the plain DID (no `#fragment`) as `serviceDid` — the fragment form only belongs in your DID doc's service entry. 41 - 42 - ## Unified `listRecords` 43 - 44 - | Call | Returns | 45 - |---|---| 46 - | no auth, no `spaceUri` | public only | 47 - | `?spaceUri=…` + JWT | one space (ACL-gated) | 48 - | JWT, no `spaceUri` | public **unioned** with every space the caller is a member of | 49 - 50 - Filters, sorts, hydration, and references work across all three. Records from a space carry a `space: <spaceUri>` field. 51 - 52 - ## Invites 53 - 54 - Stored as hashed tokens. Redemption is a single atomic UPDATE that checks `!revoked && !expired && !exhausted`. Generate, hand out the plaintext once, verify later. 55 - 56 - ## XRPCs 57 - 58 - - `com.example.space.create | get | list | delete` 59 - - `com.example.space.putRecord | deleteRecord | listRecords | getRecord` 60 - - `com.example.space.invite.create | redeem | revoke | list` 61 - - `com.example.space.listMembers | removeMember` 62 - 63 - ## What's not here 64 - 65 - - No E2EE (data is operator-readable). 66 - - No FTS on `?spaceUri=…` yet. 67 - - No per-space sharding — one DB, one operator. 68 - - Not a long-term replacement for real atproto permissioned repos. 69 - 70 - The design follows Daniel Holmgren's [permissioned data rough spec](https://dholms.leaflet.pub/3mhj6bcqats2o). The goal is that when real atproto permissioned repos ship, migration is mostly data movement — the API your app speaks doesn't change.
-54
docs/05-communities.md
··· 1 - # Communities 2 - 3 - Group-controlled atproto DIDs. A community is a DID whose signing/rotation keys are held by the appview on behalf of multiple members, with tiered access levels. Built on top of [spaces](./04-spaces.md). 4 - 5 - ## When to use this 6 - 7 - When you want atproto records published under a *shared* identity — a team, a project, a channel — not a single user. Think: a group's published calendar events, a community's published posts. 8 - 9 - ## Two modes 10 - 11 - - **Minted** — contrail creates a fresh `did:plc` for the community, holds the signing + rotation keys, publishes from it. 12 - - **Adopted** — contrail takes over an existing DID whose rotation keys were handed over by the owner. 13 - 14 - Either way, the result is the same: a DID that multiple members can act through, gated by access levels. 15 - 16 - ## Access levels 17 - 18 - Each member has a level (ranked). Levels map to write permissions. Owners can grant/revoke levels. Two reserved levels exist: `owner` and `member`. Your deployment defines the rest: 19 - 20 - ```ts 21 - community: { 22 - masterKey: ENV.COMMUNITY_MASTER_KEY, // 32-byte encryption key for stored credentials 23 - serviceDid: "did:web:example.com", 24 - levels: ["admin", "moderator"], // ranked, highest-first 25 - } 26 - ``` 27 - 28 - Stored credentials (app passwords for adopted communities, signing keys for minted) are envelope-encrypted with `masterKey`. Never ship the placeholder. 29 - 30 - ## How it composes with spaces 31 - 32 - A community *owns* spaces. Members of the community get access to community-owned spaces based on their level. Grant access per-space per-level: 33 - 34 - ``` 35 - community.space.grant { spaceUri, level: "admin", perms: "write" } 36 - ``` 37 - 38 - The spaces layer stays ignorant of access levels — it just sees "this DID is a member with these perms." The community layer projects member × level → space perms. 39 - 40 - ## XRPCs 41 - 42 - - `com.example.community.mint | adopt | list | delete` 43 - - `com.example.community.invite.create | redeem | revoke | list` 44 - - `com.example.community.setAccessLevel | revoke | listMembers` 45 - - `com.example.community.space.create | grant | revoke | ...` — community-owned spaces 46 - - `com.example.community.putRecord | deleteRecord` — publish records as the community DID 47 - 48 - ## What's not here 49 - 50 - - No per-record per-level ACLs. Model as spaces. 51 - - No auto-rotation on key compromise yet. 52 - - Adoption is irreversible without manual key surrender back to the owner. 53 - 54 - The design follows zicklag's [Arbiter design sketch](https://zicklag.leaflet.pub/3mjrvb5pul224) for group management on atproto. The post is an early design note; our implementation will track it as the spec firms up.
+74
docs/05-spaces.md
··· 1 + # Spaces 2 + 3 + Auth-gated store for records that can't live on public PDSes — private events, invite-only groups, members-only chat. Opt-in; zero cost if you don't enable it. 4 + 5 + ## Mental model 6 + 7 + > A **space** is a bag of records with one lock. The **member list** says who has the key. 8 + 9 + - One owner (DID), one type (NSID), one key. Identified by `at://<owner>/<type>/<key>`. 10 + - Every member (including owner) has read + write inside the space. Delete is scoped to your own records — no one can remove records they didn't author, owner included. To wipe everything, delete the space. 11 + - Optional **app policy** gates which OAuth clients can act in the space. 12 + 13 + Every permission boundary is its own space. No nested ACLs. Richer roles = more spaces or app-layer checks. 14 + 15 + ## Enable 16 + 17 + ```ts 18 + import type { ContrailConfig } from "@atmo-dev/contrail"; 19 + 20 + const config: ContrailConfig = { 21 + namespace: "com.example", 22 + collections: { /* ... */ }, 23 + spaces: { 24 + type: "com.example.event.space", 25 + serviceDid: "did:web:example.com", 26 + }, 27 + }; 28 + ``` 29 + 30 + Each collection gets a parallel `spaces_records_<short>` table. Opt out per-collection: 31 + 32 + ```ts 33 + public_only: { collection: "com.example.public", allowInSpaces: false } 34 + ``` 35 + 36 + ## Auth 37 + 38 + Spaces use the standard contrail auth surface — service-auth JWTs for third-party apps, in-process server clients for your own loaders, invite tokens for anonymous read-grant links. See [Auth](./04-auth.md) for the full picture. 39 + 40 + Space-specific wiring: 41 + 42 + - `serviceDid` in the config is the `aud` contrail expects on incoming JWTs. Plain DID, no `#fragment`. 43 + - Apps acting in a space send `Atproto-Proxy: <serviceDid>#<service-id-from-your-did-doc>` so the user's PDS routes correctly. 44 + - Invite redemption via service-auth JWT grants membership; via `?inviteToken=...` query param grants read-only bearer access to that space. 45 + 46 + ## Unified `listRecords` 47 + 48 + | Call | Returns | 49 + |---|---| 50 + | no auth, no `spaceUri` | public only | 51 + | `?spaceUri=…` + JWT | one space (ACL-gated) | 52 + | JWT, no `spaceUri` | public **unioned** with every space the caller is a member of | 53 + 54 + Filters, sorts, hydration, and references work across all three. Records from a space carry a `space: <spaceUri>` field. 55 + 56 + ## Invites 57 + 58 + First-class primitive — see [Auth § Invite tokens](./04-auth.md#invite-tokens) for the mechanism. Space-specific: create via `com.example.space.invite.create`, redeem via `.redeem` (membership grant) or `?inviteToken=...` query param (read-only bearer grant). 59 + 60 + ## XRPCs 61 + 62 + - `com.example.space.create | get | list | delete` 63 + - `com.example.space.putRecord | deleteRecord | listRecords | getRecord` 64 + - `com.example.space.invite.create | redeem | revoke | list` 65 + - `com.example.space.listMembers | removeMember` 66 + 67 + ## What's not here 68 + 69 + - No E2EE (data is operator-readable). 70 + - No FTS on `?spaceUri=…` yet. 71 + - No per-space sharding — one DB, one operator. 72 + - Not a long-term replacement for real atproto permissioned repos. 73 + 74 + The design follows Daniel Holmgren's [permissioned data rough spec](https://dholms.leaflet.pub/3mhj6bcqats2o). The goal is that when real atproto permissioned repos ship, migration is mostly data movement — the API your app speaks doesn't change.
+54
docs/06-communities.md
··· 1 + # Communities 2 + 3 + Group-controlled atproto DIDs. A community is a DID whose signing/rotation keys are held by the appview on behalf of multiple members, with tiered access levels. Built on top of [spaces](./05-spaces.md). 4 + 5 + ## When to use this 6 + 7 + When you want atproto records published under a *shared* identity — a team, a project, a channel — not a single user. Think: a group's published calendar events, a community's published posts. 8 + 9 + ## Two modes 10 + 11 + - **Minted** — contrail creates a fresh `did:plc` for the community, holds the signing + rotation keys, publishes from it. 12 + - **Adopted** — contrail takes over an existing DID whose rotation keys were handed over by the owner. 13 + 14 + Either way, the result is the same: a DID that multiple members can act through, gated by access levels. 15 + 16 + ## Access levels 17 + 18 + Each member has a level (ranked). Levels map to write permissions. Owners can grant/revoke levels. Two reserved levels exist: `owner` and `member`. Your deployment defines the rest: 19 + 20 + ```ts 21 + community: { 22 + masterKey: ENV.COMMUNITY_MASTER_KEY, // 32-byte encryption key for stored credentials 23 + serviceDid: "did:web:example.com", 24 + levels: ["admin", "moderator"], // ranked, highest-first 25 + } 26 + ``` 27 + 28 + Stored credentials (app passwords for adopted communities, signing keys for minted) are envelope-encrypted with `masterKey`. Never ship the placeholder. 29 + 30 + ## How it composes with spaces 31 + 32 + A community *owns* spaces. Members of the community get access to community-owned spaces based on their access level. Grant access per-space per-level: 33 + 34 + ``` 35 + community.space.grant { spaceUri, subject: { did: "did:plc:..." }, accessLevel: "admin" } 36 + ``` 37 + 38 + The spaces layer stays ignorant of access levels — it just sees "this DID is a member." The community layer projects member × level → membership in specific spaces. Once a DID is a member of a space (through a community grant or otherwise), they have full read + write inside it. 39 + 40 + ## XRPCs 41 + 42 + - `com.example.community.mint | adopt | list | delete` 43 + - `com.example.community.invite.create | redeem | revoke | list` 44 + - `com.example.community.setAccessLevel | revoke | listMembers` 45 + - `com.example.community.space.create | grant | revoke | ...` — community-owned spaces 46 + - `com.example.community.putRecord | deleteRecord` — publish records as the community DID 47 + 48 + ## What's not here 49 + 50 + - No per-record per-level ACLs. Model as spaces. 51 + - No auto-rotation on key compromise yet. 52 + - Adoption is irreversible without manual key surrender back to the owner. 53 + 54 + The design follows zicklag's [Arbiter design sketch](https://zicklag.leaflet.pub/3mjrvb5pul224) for group management on atproto. The post is an early design note; our implementation will track it as the spec firms up.
-93
docs/06-sync.md
··· 1 - # Sync 2 - 3 - Client-side reactive store over contrail's `watchRecords` endpoints. Subscribes once, reconciles forever, ships with optimistic updates and an optional IndexedDB cache. 4 - 5 - Lives in its own package: 6 - 7 - ```bash 8 - pnpm add @atmo-dev/contrail-sync 9 - ``` 10 - 11 - ## Basic use 12 - 13 - ```ts 14 - import { createWatchStore } from "@atmo-dev/contrail-sync"; 15 - 16 - const store = createWatchStore({ 17 - url: "/xrpc/com.example.message.watchRecords?roomUri=at://...", 18 - transport: "sse", // or "ws" 19 - }); 20 - 21 - store.subscribe(({ records, status }) => { /* re-render */ }); 22 - store.start(); 23 - ``` 24 - 25 - Framework-agnostic — wrap it in Svelte `$state`, React `useSyncExternalStore`, Vue `ref`, whatever. 26 - 27 - ## Transports 28 - 29 - - **SSE** (default) — one HTTP request, simplest. Works everywhere. 30 - - **WS** — a two-step handshake: HTTP GET returns a snapshot + watch-scoped ticket, then you upgrade to WS. On Cloudflare, the WS terminates on a Durable Object that hibernates idle connections. Same event stream either way. 31 - 32 - ## Authenticated watches 33 - 34 - Pass `mintTicket` for any non-public endpoint. One-shot string for SSR-minted tickets, function for fresh tickets per reconnect: 35 - 36 - ```ts 37 - mintTicket: async () => (await fetch("/api/ticket")).then((r) => r.text()), 38 - ``` 39 - 40 - Tickets are minted server-side via `com.example.realtime.ticket` (or any app-specific route). 41 - 42 - ## Optimistic updates 43 - 44 - ```ts 45 - store.addOptimistic({ rkey, did, record: { text: "hi" } }); 46 - // later, on mutation failure: 47 - store.markFailed(rkey, err); 48 - // or explicit rollback: 49 - store.removeOptimistic(rkey); 50 - ``` 51 - 52 - When a real record with the same `rkey` arrives via the stream, the optimistic entry is dropped automatically. 53 - 54 - ## IndexedDB cache 55 - 56 - Instant first paint from last session's records: 57 - 58 - ```ts 59 - import { createIndexedDBCache } from "@atmo-dev/contrail-sync/cache-idb"; 60 - 61 - createWatchStore({ 62 - url, 63 - cache: createIndexedDBCache(), 64 - cacheMaxRecords: 200, 65 - }); 66 - ``` 67 - 68 - Cached records show immediately; the live snapshot reconciles when the connection opens. 69 - 70 - ## Server-side config 71 - 72 - Enable `watchRecords` emission in your Contrail config: 73 - 74 - ```ts 75 - realtime: { 76 - ticketSecret: ENV.REALTIME_TICKET_SECRET, // 32 bytes 77 - pubsub: new DurableObjectPubSub(env.PUBSUB), // or in-memory for dev 78 - } 79 - ``` 80 - 81 - See [Indexing](./01-indexing.md) for the full config surface. 82 - 83 - ## Lifecycle 84 - 85 - ``` 86 - idle → connecting → snapshot → live 87 - ↓ (disconnect) 88 - reconnecting → snapshot → live 89 - ↓ (stop) 90 - closed 91 - ``` 92 - 93 - Stale records stay visible across reconnects until the fresh snapshot arrives, at which point anything the server didn't re-send is evicted. Survives offline periods cleanly.
-65
docs/07-examples.md
··· 1 - # Examples 2 - 3 - Every example lives in [`apps/`](https://github.com/flo-bit/contrail/tree/main/apps) and pins contrail as `workspace:*`. Clone the repo, `pnpm install`, and each one runs. 4 - 5 - ## `rsvp-atmo` — the reference deployment 6 - 7 - [`apps/rsvp-atmo`](https://github.com/flo-bit/contrail/tree/main/apps/rsvp-atmo) 8 - 9 - Cloudflare Workers + D1. Indexes `community.lexicon.calendar.event` and `rsvp`. Exposes the full spaces + community + realtime surface. Cron-driven Jetstream ingestion every minute. 10 - 11 - Use this if you're building on Workers and want a starting point that already has deploy config wired up. 12 - 13 - ```bash 14 - pnpm --filter rsvp-atmo dev # local wrangler + auto-cron 15 - pnpm --filter rsvp-atmo deploy # requires D1 database created 16 - pnpm --filter rsvp-atmo sync # discover + backfill against D1 17 - ``` 18 - 19 - ## `group-chat` — full app showcase 20 - 21 - [`apps/group-chat`](https://github.com/flo-bit/contrail/tree/main/apps/group-chat) 22 - 23 - SvelteKit + Cloudflare Workers. The one that exercises everything: permissioned spaces for private rooms, community-controlled DIDs for groups, client-side `contrail-sync` for reactive messages, Durable Object-hibernated WebSockets for realtime delivery, OAuth-based login. 24 - 25 - This is the canonical "what can contrail do" demo. If you're trying to understand how the pieces fit together end to end, read this app's code before anything else. 26 - 27 - ```bash 28 - pnpm --filter sveltekit-group-chat dev 29 - ``` 30 - 31 - ## `postgres` — Node + PG minimal 32 - 33 - [`apps/postgres`](https://github.com/flo-bit/contrail/tree/main/apps/postgres) 34 - 35 - The smallest possible Node deployment. Docker Compose for Postgres, three scripts: `sync` (discover + backfill), `ingest` (persistent Jetstream), `serve` (HTTP handler). Skips spaces/communities/realtime. 36 - 37 - Use this as a template if you're running on a normal server and don't need Cloudflare's bells. 38 - 39 - ```bash 40 - cd apps/postgres 41 - docker compose up -d 42 - pnpm sync 43 - pnpm serve 44 - ``` 45 - 46 - ## `cloudflare-workers` — minimal Workers 47 - 48 - [`apps/cloudflare-workers`](https://github.com/flo-bit/contrail/tree/main/apps/cloudflare-workers) 49 - 50 - The simplest working Worker. One collection (events), one HTTP handler, cron-driven ingest. No spaces, no communities. Good for reading top-to-bottom in one sitting to see what contrail does at minimum. 51 - 52 - ## `sveltekit-cloudflare-workers` — SvelteKit Statusphere 53 - 54 - [`apps/sveltekit-cloudflare-workers`](https://github.com/flo-bit/contrail/tree/main/apps/sveltekit-cloudflare-workers) 55 - 56 - A Statusphere-style SvelteKit app with OAuth login, contrail-indexed public records, and Cloudflare adapter. No spaces/communities — think "atproto blog or status post UI." Useful as a scaffold for public-only apps. 57 - 58 - ## Choosing a starting point 59 - 60 - | Need | Start from | 61 - |---|---| 62 - | "Just index some records" | `cloudflare-workers` or `postgres` | 63 - | "Index + SvelteKit UI, public only" | `sveltekit-cloudflare-workers` | 64 - | "Private rooms / group chat / full stack" | `group-chat` | 65 - | "Calendar-ish domain, Workers deploy" | `rsvp-atmo` |
+93
docs/07-sync.md
··· 1 + # Sync 2 + 3 + Client-side reactive store over contrail's `watchRecords` endpoints. Subscribes once, reconciles forever, ships with optimistic updates and an optional IndexedDB cache. 4 + 5 + Lives in its own package: 6 + 7 + ```bash 8 + pnpm add @atmo-dev/contrail-sync 9 + ``` 10 + 11 + ## Basic use 12 + 13 + ```ts 14 + import { createWatchStore } from "@atmo-dev/contrail-sync"; 15 + 16 + const store = createWatchStore({ 17 + url: "/xrpc/com.example.message.watchRecords?roomUri=at://...", 18 + transport: "sse", // or "ws" 19 + }); 20 + 21 + store.subscribe(({ records, status }) => { /* re-render */ }); 22 + store.start(); 23 + ``` 24 + 25 + Framework-agnostic — wrap it in Svelte `$state`, React `useSyncExternalStore`, Vue `ref`, whatever. 26 + 27 + ## Transports 28 + 29 + - **SSE** (default) — one HTTP request, simplest. Works everywhere. 30 + - **WS** — a two-step handshake: HTTP GET returns a snapshot + watch-scoped ticket, then you upgrade to WS. On Cloudflare, the WS terminates on a Durable Object that hibernates idle connections. Same event stream either way. 31 + 32 + ## Authenticated watches 33 + 34 + Pass `mintTicket` for any non-public endpoint. One-shot string for SSR-minted tickets, function for fresh tickets per reconnect: 35 + 36 + ```ts 37 + mintTicket: async () => (await fetch("/api/ticket")).then((r) => r.text()), 38 + ``` 39 + 40 + Tickets are minted server-side via `com.example.realtime.ticket` (or any app-specific route). 41 + 42 + ## Optimistic updates 43 + 44 + ```ts 45 + store.addOptimistic({ rkey, did, record: { text: "hi" } }); 46 + // later, on mutation failure: 47 + store.markFailed(rkey, err); 48 + // or explicit rollback: 49 + store.removeOptimistic(rkey); 50 + ``` 51 + 52 + When a real record with the same `rkey` arrives via the stream, the optimistic entry is dropped automatically. 53 + 54 + ## IndexedDB cache 55 + 56 + Instant first paint from last session's records: 57 + 58 + ```ts 59 + import { createIndexedDBCache } from "@atmo-dev/contrail-sync/cache-idb"; 60 + 61 + createWatchStore({ 62 + url, 63 + cache: createIndexedDBCache(), 64 + cacheMaxRecords: 200, 65 + }); 66 + ``` 67 + 68 + Cached records show immediately; the live snapshot reconciles when the connection opens. 69 + 70 + ## Server-side config 71 + 72 + Enable `watchRecords` emission in your Contrail config: 73 + 74 + ```ts 75 + realtime: { 76 + ticketSecret: ENV.REALTIME_TICKET_SECRET, // 32 bytes 77 + pubsub: new DurableObjectPubSub(env.PUBSUB), // or in-memory for dev 78 + } 79 + ``` 80 + 81 + See [Indexing](./01-indexing.md) for the full config surface. 82 + 83 + ## Lifecycle 84 + 85 + ``` 86 + idle → connecting → snapshot → live 87 + ↓ (disconnect) 88 + reconnecting → snapshot → live 89 + ↓ (stop) 90 + closed 91 + ``` 92 + 93 + Stale records stay visible across reconnects until the fresh snapshot arrives, at which point anything the server didn't re-send is evicted. Survives offline periods cleanly.
+65
docs/08-examples.md
··· 1 + # Examples 2 + 3 + Every example lives in [`apps/`](https://github.com/flo-bit/contrail/tree/main/apps) and pins contrail as `workspace:*`. Clone the repo, `pnpm install`, and each one runs. 4 + 5 + ## `rsvp-atmo` — the reference deployment 6 + 7 + [`apps/rsvp-atmo`](https://github.com/flo-bit/contrail/tree/main/apps/rsvp-atmo) 8 + 9 + Cloudflare Workers + D1. Indexes `community.lexicon.calendar.event` and `rsvp`. Exposes the full spaces + community + realtime surface. Cron-driven Jetstream ingestion every minute. 10 + 11 + Use this if you're building on Workers and want a starting point that already has deploy config wired up. 12 + 13 + ```bash 14 + pnpm --filter rsvp-atmo dev # local wrangler + auto-cron 15 + pnpm --filter rsvp-atmo deploy # requires D1 database created 16 + pnpm --filter rsvp-atmo sync # discover + backfill against D1 17 + ``` 18 + 19 + ## `group-chat` — full app showcase 20 + 21 + [`apps/group-chat`](https://github.com/flo-bit/contrail/tree/main/apps/group-chat) 22 + 23 + SvelteKit + Cloudflare Workers. The one that exercises everything: permissioned spaces for private rooms, community-controlled DIDs for groups, client-side `contrail-sync` for reactive messages, Durable Object-hibernated WebSockets for realtime delivery, OAuth-based login. 24 + 25 + This is the canonical "what can contrail do" demo. If you're trying to understand how the pieces fit together end to end, read this app's code before anything else. 26 + 27 + ```bash 28 + pnpm --filter sveltekit-group-chat dev 29 + ``` 30 + 31 + ## `postgres` — Node + PG minimal 32 + 33 + [`apps/postgres`](https://github.com/flo-bit/contrail/tree/main/apps/postgres) 34 + 35 + The smallest possible Node deployment. Docker Compose for Postgres, three scripts: `sync` (discover + backfill), `ingest` (persistent Jetstream), `serve` (HTTP handler). Skips spaces/communities/realtime. 36 + 37 + Use this as a template if you're running on a normal server and don't need Cloudflare's bells. 38 + 39 + ```bash 40 + cd apps/postgres 41 + docker compose up -d 42 + pnpm sync 43 + pnpm serve 44 + ``` 45 + 46 + ## `cloudflare-workers` — minimal Workers 47 + 48 + [`apps/cloudflare-workers`](https://github.com/flo-bit/contrail/tree/main/apps/cloudflare-workers) 49 + 50 + The simplest working Worker. One collection (events), one HTTP handler, cron-driven ingest. No spaces, no communities. Good for reading top-to-bottom in one sitting to see what contrail does at minimum. 51 + 52 + ## `sveltekit-cloudflare-workers` — SvelteKit Statusphere 53 + 54 + [`apps/sveltekit-cloudflare-workers`](https://github.com/flo-bit/contrail/tree/main/apps/sveltekit-cloudflare-workers) 55 + 56 + A Statusphere-style SvelteKit app with OAuth login, contrail-indexed public records, and Cloudflare adapter. No spaces/communities — think "atproto blog or status post UI." Useful as a scaffold for public-only apps. 57 + 58 + ## Choosing a starting point 59 + 60 + | Need | Start from | 61 + |---|---| 62 + | "Just index some records" | `cloudflare-workers` or `postgres` | 63 + | "Index + SvelteKit UI, public only" | `sveltekit-cloudflare-workers` | 64 + | "Private rooms / group chat / full stack" | `group-chat` | 65 + | "Calendar-ish domain, Workers deploy" | `rsvp-atmo` |
+2 -2
apps/cloudflare-workers/README.md
··· 39 39 - **add a collection:** append to `collections` in `src/contrail.config.ts`; redeploy; `pnpm contrail backfill --remote` to backfill the new one. 40 40 - **add full-text search:** `searchable: ["field1", "field2"]`, redeploy, no backfill needed (fts indexes repopulate on ingest). 41 41 - **add relations / references:** see [indexing docs](../../docs/01-indexing.md). 42 - - **private records:** see [spaces docs](../../docs/04-spaces.md). 43 - - **group-controlled DIDs:** see [communities docs](../../docs/05-communities.md). 42 + - **private records:** see [spaces docs](../../docs/05-spaces.md). 43 + - **group-controlled DIDs:** see [communities docs](../../docs/06-communities.md).
-14
apps/rsvp-atmo/Dockerfile
··· 1 - FROM node:22-alpine 2 - 3 - RUN corepack enable && corepack prepare pnpm@latest --activate 4 - 5 - WORKDIR /app 6 - 7 - COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ 8 - RUN pnpm install --frozen-lockfile 9 - 10 - COPY src/ src/ 11 - COPY app/ app/ 12 - COPY bin/ bin/ 13 - 14 - CMD ["npx", "tsx", "bin/ingest.ts"]
-35
apps/rsvp-atmo/package.json
··· 1 - { 2 - "name": "rsvp-atmo", 3 - "private": true, 4 - "version": "0.0.0", 5 - "type": "module", 6 - "description": "Reference Cloudflare Workers deployment of contrail — indexes community.lexicon.calendar.event/rsvp on D1 with the full spaces/community/realtime surface enabled.", 7 - "scripts": { 8 - "dev": "wrangler dev --test-scheduled", 9 - "dev:auto": "tsx scripts/dev.ts", 10 - "deploy": "wrangler deploy", 11 - "clean": "tsx scripts/clean.ts", 12 - "generate": "tsx src/generate.ts", 13 - "generate:pull": "contrail-lex all", 14 - "publish-lexicons": "tsx scripts/publish-lexicons.ts", 15 - "typecheck": "tsc --noEmit", 16 - "ingest": "curl -s http://localhost:8787/__scheduled?cron=*/1+*+*+*+*", 17 - "sync": "tsx src/sync.ts" 18 - }, 19 - "dependencies": { 20 - "@atmo-dev/contrail": "workspace:*" 21 - }, 22 - "devDependencies": { 23 - "@atcute/atproto": "^3.1.10", 24 - "@atcute/client": "^4.2.1", 25 - "@atcute/lex-cli": "^2.5.3", 26 - "@atcute/lexicon-doc": "^2.1.2", 27 - "@atcute/lexicons": "^1.2.9", 28 - "@atmo-dev/contrail-lexicons": "workspace:*", 29 - "@cloudflare/workers-types": "^4.20250124.0", 30 - "@types/node": "^25.5.0", 31 - "tsx": "^4.21.0", 32 - "typescript": "^5.7.3", 33 - "wrangler": "^4.63.0" 34 - } 35 - }
-7
apps/rsvp-atmo/tsconfig.json
··· 1 - { 2 - "extends": "../../tsconfig.base.json", 3 - "compilerOptions": { 4 - "types": ["@cloudflare/workers-types", "node"] 5 - }, 6 - "include": ["src", "scripts"] 7 - }
-21
apps/rsvp-atmo/wrangler.jsonc
··· 1 - { 2 - "$schema": "node_modules/wrangler/config-schema.json", 3 - "name": "contrail", 4 - "main": "src/worker.ts", 5 - "compatibility_date": "2025-12-25", 6 - "observability": { 7 - "enabled": true 8 - }, 9 - "d1_databases": [ 10 - { 11 - "binding": "DB", 12 - "database_name": "contrail", 13 - "database_id": "29cf6e32-d0b9-4646-ac52-bfbeef085c1a", 14 - } 15 - ], 16 - "triggers": { 17 - "crons": [ 18 - "*/1 * * * *" 19 - ] 20 - } 21 - }
+3 -2
docs/frameworks/sveltekit-cloudflare.md
··· 229 229 - [Indexing](../01-indexing.md) — config options, adapter choices 230 230 - [Querying](../02-querying.md) — filters, sorts, hydration, search 231 231 - [Lexicons](../03-lexicons.md) — generate TS types for your XRPC surface 232 - - [Spaces](../04-spaces.md) / [Communities](../05-communities.md) — private records + group-controlled DIDs, which both slot into the same handler you just mounted 233 - - [Sync](../06-sync.md) — reactive client-side subscriptions (`createWatchStore`) wrapped in Svelte `$state` 232 + - [Auth](../04-auth.md) — service-auth JWTs, invite tokens, watch tickets, OAuth permission sets 233 + - [Spaces](../05-spaces.md) / [Communities](../06-communities.md) — private records + group-controlled DIDs, which both slot into the same handler you just mounted 234 + - [Sync](../07-sync.md) — reactive client-side subscriptions (`createWatchStore`) wrapped in Svelte `$state` 234 235 235 236 ## Common gotchas 236 237
+2 -2
packages/contrail/PERMISSIONED_DATA.md
··· 16 16 17 17 - Each space has one owner (DID), one type (classifying NSID), one key (string). Identified by an at-uri: `at://<owner>/<type>/<key>`. 18 18 - A space holds records of *any* NSID. Same role a PDS repo plays, scoped to a shared context. 19 - - Each member has a **perms** value: `"read"` or `"write"`. The space owner is implicit `write`. 19 + - Every member has full read + write inside the space. No per-member permission axis — membership is the whole ACL. 20 20 - Each space has an optional **app policy**: `{ mode: "allow" | "deny", apps: [client_id, …] }` — gates which OAuth clients can act in the space. 21 21 22 22 That's it. Every feature (private channels, invite-only threads, shared albums) is one-space-per-permission-boundary with records of whatever NSIDs the app defines. If you need finer-grained access (e.g. "admins" vs "members"), model it as multiple spaces or enforce at the app layer. ··· 25 25 26 26 **Each permission boundary = its own space.** No nested ACLs, no per-record permissions, no per-collection policies. Matches the blog's model; keeps the library generic. 27 27 28 - **Read/write as the only permission axis.** The library enforces: read = any member, write = members with `"write"` (owner always). Apps that need richer roles layer them on top of multiple spaces or check `clientId` / authorDid in their handlers. 28 + **Membership is the whole ACL.** Being in the member list = full read + write. No per-member permission granularity. Apps that need richer roles model them as multiple spaces or check `clientId` / `authorDid` in their handlers. 29 29 30 30 **atproto service-auth JWTs.** Verification uses `@atcute/xrpc-server/auth.ServiceJwtVerifier`: validates signature against issuer's DID doc, checks `aud` matches the configured service DID, checks `lxm` covers the method. Same path third-party apps and our own code go through. 31 31
-26
apps/rsvp-atmo/scripts/clean.ts
··· 1 - /** 2 - * Remove all generated files. 3 - * 4 - * Usage: npx tsx scripts/clean.ts 5 - */ 6 - 7 - import { rmSync } from "fs"; 8 - import { join, dirname } from "path"; 9 - import { fileURLToPath } from "url"; 10 - 11 - const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); 12 - 13 - const targets = [ 14 - ".wrangler", 15 - "lex.config.js", 16 - "lexicons/pulled", 17 - "lexicons/generated", 18 - "src/lexicon-types", 19 - ]; 20 - 21 - for (const target of targets) { 22 - rmSync(join(ROOT, target), { recursive: true, force: true }); 23 - console.log(` removed ${target}`); 24 - } 25 - 26 - console.log("Done.");
-39
apps/rsvp-atmo/scripts/dev.ts
··· 1 - /** 2 - * Start wrangler dev and auto-trigger the cron every 60s. 3 - * 4 - * Usage: npx tsx scripts/dev.ts 5 - */ 6 - 7 - import { spawn } from "child_process"; 8 - 9 - async function main() { 10 - const wrangler = spawn("npx", ["wrangler", "dev", "--test-scheduled"], { 11 - stdio: "inherit", 12 - shell: true, 13 - }); 14 - 15 - // Wait for wrangler to start 16 - await new Promise((resolve) => setTimeout(resolve, 3000)); 17 - 18 - console.log("Auto-ingestion started (every 60s)"); 19 - 20 - const interval = setInterval(async () => { 21 - try { 22 - await fetch("http://localhost:8787/__scheduled?cron=*/1+*+*+*+*"); 23 - } catch { 24 - // wrangler not ready yet or shutting down 25 - } 26 - }, 60_000); 27 - 28 - // Trigger once immediately 29 - try { 30 - await fetch("http://localhost:8787/__scheduled?cron=*/1+*+*+*+*"); 31 - } catch {} 32 - 33 - wrangler.on("exit", () => { 34 - clearInterval(interval); 35 - process.exit(); 36 - }); 37 - } 38 - 39 - main();
-42
apps/rsvp-atmo/scripts/publish-lexicons.ts
··· 1 - /** 2 - * CLI wrapper for `publishLexicons` — publishes contrail's own generated 3 - * lexicons (under `lexicons/generated/`) to the logged-in account's PDS. 4 - * 5 - * Usage: 6 - * LEXICON_ACCOUNT_IDENTIFIER=you.bsky.social \ 7 - * LEXICON_ACCOUNT_PASSWORD=xxxx-xxxx-xxxx-xxxx \ 8 - * pnpm publish-lexicons 9 - * 10 - * pnpm publish-lexicons <identifier> <app-password> 11 - */ 12 - 13 - import { join } from "node:path"; 14 - import { publishLexicons } from "@atmo-dev/contrail-lexicons"; 15 - 16 - const ROOT = join(import.meta.dirname, ".."); 17 - 18 - async function main(): Promise<void> { 19 - const identifier = process.argv[2] ?? process.env.LEXICON_ACCOUNT_IDENTIFIER; 20 - const password = process.argv[3] ?? process.env.LEXICON_ACCOUNT_PASSWORD; 21 - 22 - if (!identifier || !password) { 23 - console.error( 24 - "Usage: pnpm publish-lexicons <handle-or-did> <app-password>\n" + 25 - " (or set LEXICON_ACCOUNT_IDENTIFIER and LEXICON_ACCOUNT_PASSWORD env vars)\n\n" + 26 - "Generate an app password at https://bsky.app/settings/app-passwords\n" + 27 - "(or equivalent on your PDS)." 28 - ); 29 - process.exit(1); 30 - } 31 - 32 - await publishLexicons({ 33 - generatedDir: join(ROOT, "lexicons", "generated"), 34 - identifier, 35 - password, 36 - }); 37 - } 38 - 39 - main().catch((err) => { 40 - console.error(err); 41 - process.exit(1); 42 - });
-65
apps/rsvp-atmo/src/contrail.config.ts
··· 1 - import type { ContrailConfig } from "@atmo-dev/contrail"; 2 - 3 - export const config: ContrailConfig = { 4 - namespace: "rsvp.atmo", 5 - jetstreams: ["wss://jetstream1.us-east.bsky.network"], 6 - spaces: { 7 - type: "tools.atmo.event.space", 8 - serviceDid: "did:web:rsvp.atmo", 9 - }, 10 - community: { 11 - // 32-byte master key for envelope-encrypting stored credentials (app 12 - // passwords + minted signing/rotation keys). Provide via env: hex or 13 - // base64 encoded. For Cloudflare Workers, wire from env.COMMUNITY_MASTER_KEY 14 - // via a config factory pattern. The placeholder below is fine for `pnpm 15 - // generate` (which never instantiates the cipher) but must be replaced 16 - // before starting the server. 17 - masterKey: 18 - (typeof process !== "undefined" ? process.env.COMMUNITY_MASTER_KEY : undefined) ?? 19 - "placeholder-set-me-before-running-server-not-at-build-time", 20 - }, 21 - collections: { 22 - event: { 23 - collection: "community.lexicon.calendar.event", 24 - queryable: { 25 - mode: {}, 26 - name: {}, 27 - status: {}, 28 - startsAt: { type: "range" }, 29 - endsAt: { type: "range" }, 30 - createdAt: { type: "range" }, 31 - }, 32 - searchable: ["name", "description"], 33 - relations: { 34 - rsvps: { 35 - collection: "rsvp", 36 - groupBy: "status", 37 - count: true, 38 - countDistinct: "did", 39 - groups: { 40 - interested: "community.lexicon.calendar.rsvp#interested", 41 - going: "community.lexicon.calendar.rsvp#going", 42 - notgoing: "community.lexicon.calendar.rsvp#notgoing", 43 - }, 44 - }, 45 - }, 46 - }, 47 - rsvp: { 48 - collection: "community.lexicon.calendar.rsvp", 49 - queryable: { 50 - status: {}, 51 - "subject.uri": {}, 52 - }, 53 - references: { 54 - event: { 55 - collection: "event", 56 - field: "subject.uri", 57 - }, 58 - }, 59 - }, 60 - }, 61 - 62 - profiles: [ 63 - "app.bsky.actor.profile" 64 - ] 65 - };
-18
apps/rsvp-atmo/src/generate.ts
··· 1 - /** 2 - * Generates lexicon files and lex.config.js from config. 3 - * 4 - * Usage: pnpm generate 5 - */ 6 - import { join, dirname } from "path"; 7 - import { fileURLToPath } from "url"; 8 - import { config } from "./contrail.config"; 9 - import { generateLexicons } from "@atmo-dev/contrail-lexicons"; 10 - 11 - const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), ".."); 12 - 13 - generateLexicons({ 14 - config, 15 - rootDir: ROOT_DIR, 16 - outputDir: join(ROOT_DIR, "lexicons", "generated"), 17 - writeRuntimeFiles: true, 18 - });
-68
apps/rsvp-atmo/src/sync.ts
··· 1 - /** 2 - * Discover users from relays and backfill their records from PDS. 3 - * 4 - * Usage: 5 - * pnpm sync # local D1 6 - * pnpm sync --remote # prod D1 7 - */ 8 - import { Contrail } from "@atmo-dev/contrail"; 9 - import { config } from "./contrail.config"; 10 - import { getPlatformProxy } from "wrangler"; 11 - 12 - function elapsed(start: number): string { 13 - const ms = Date.now() - start; 14 - if (ms < 1000) return `${ms}ms`; 15 - if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; 16 - const mins = Math.floor(ms / 60_000); 17 - const secs = ((ms % 60_000) / 1000).toFixed(0); 18 - return `${mins}m ${secs}s`; 19 - } 20 - 21 - async function main() { 22 - const remote = process.argv.includes("--remote"); 23 - const syncStart = Date.now(); 24 - 25 - console.log(`=== Sync (${remote ? "remote/prod" : "local"} D1) ===\n`); 26 - 27 - const { env, dispose } = await getPlatformProxy<{ DB: D1Database }>({ 28 - environment: remote ? "production" : undefined, 29 - }); 30 - 31 - const contrail = new Contrail({ ...config, db: env.DB }); 32 - 33 - try { 34 - await contrail.init(); 35 - 36 - console.log("--- Discovery ---"); 37 - const discoveryStart = Date.now(); 38 - const discovered = await contrail.discover(); 39 - console.log(` Done: ${discovered.length} users in ${elapsed(discoveryStart)}\n`); 40 - 41 - console.log("--- Backfill ---"); 42 - const backfillStart = Date.now(); 43 - const total = await contrail.backfill({ 44 - concurrency: 100, 45 - onProgress: ({ records, usersComplete, usersTotal, usersFailed }) => { 46 - const secs = (Date.now() - backfillStart) / 1000; 47 - const rate = secs > 0 ? Math.round(records / secs) : 0; 48 - const failStr = usersFailed > 0 ? ` | ${usersFailed} failed` : ""; 49 - process.stdout.write( 50 - `\r ${records} records | ${usersComplete}/${usersTotal} users | ${rate}/s | ${elapsed(backfillStart)}${failStr} ` 51 - ); 52 - }, 53 - }); 54 - process.stdout.write("\n"); 55 - console.log(` Done: ${total} records in ${elapsed(backfillStart)}\n`); 56 - 57 - console.log(`=== Finished in ${elapsed(syncStart)} ===`); 58 - console.log(` Discovered: ${discovered.length} users`); 59 - console.log(` Backfilled: ${total} records`); 60 - } finally { 61 - await dispose(); 62 - } 63 - } 64 - 65 - main().catch((err) => { 66 - console.error(err); 67 - process.exit(1); 68 - });
-30
apps/rsvp-atmo/src/worker.ts
··· 1 - import { Contrail } from "@atmo-dev/contrail"; 2 - import { createHandler } from "@atmo-dev/contrail/server"; 3 - import { config } from "./contrail.config"; 4 - 5 - const contrail = new Contrail(config); 6 - const handle = createHandler(contrail); 7 - 8 - let initialized = false; 9 - 10 - export default { 11 - async fetch(request: Request, env: { DB: D1Database }) { 12 - if (!initialized) { 13 - await contrail.init(env.DB); 14 - initialized = true; 15 - } 16 - return handle(request, env.DB); 17 - }, 18 - 19 - async scheduled( 20 - _event: ScheduledEvent, 21 - env: { DB: D1Database }, 22 - ctx: ExecutionContext 23 - ) { 24 - if (!initialized) { 25 - await contrail.init(env.DB); 26 - initialized = true; 27 - } 28 - ctx.waitUntil(contrail.ingest({}, env.DB)); 29 - }, 30 - };
+17 -4
packages/contrail/tests/spaces-acl.test.ts
··· 22 22 } 23 23 24 24 describe("spaces acl", () => { 25 - it("owner can read/write/delete without a member row", () => { 25 + it("owner can read/write without a member row", () => { 26 26 const s = mkSpace(); 27 - for (const op of ["read", "write", "delete"] as const) { 27 + for (const op of ["read", "write"] as const) { 28 28 const r = checkAccess({ 29 29 op, 30 30 space: s, ··· 33 33 }); 34 34 expect(r.allow).toBe(true); 35 35 } 36 + }); 37 + 38 + it("owner can delete own record without a member row", () => { 39 + const s = mkSpace(); 40 + const r = checkAccess({ 41 + op: "delete", 42 + space: s, 43 + callerDid: "did:plc:alice", 44 + member: null, 45 + targetAuthorDid: "did:plc:alice", 46 + }); 47 + expect(r.allow).toBe(true); 36 48 }); 37 49 38 50 it("non-member cannot read", () => { ··· 97 109 expect((r as any).reason).toBe("not-own-record"); 98 110 }); 99 111 100 - it("delete any: owner can delete anyone's record", () => { 112 + it("owner cannot delete someone else's record (no bypass)", () => { 101 113 const s = mkSpace(); 102 114 const r = checkAccess({ 103 115 op: "delete", ··· 106 118 member: null, 107 119 targetAuthorDid: "did:plc:bob", 108 120 }); 109 - expect(r.allow).toBe(true); 121 + expect(r.allow).toBe(false); 122 + expect((r as any).reason).toBe("not-own-record"); 110 123 }); 111 124 112 125 it("delete by non-member: denied as not-member, not not-own-record", () => {
-13
apps/rsvp-atmo/lexicons/custom/README.md
··· 1 - # Custom lexicons 2 - 3 - Place your own hand-authored lexicon JSON files here. 4 - 5 - ## Folder conventions 6 - 7 - - **`lexicons/custom/`** (this folder) — Your own hand-authored lexicons. 8 - - **`lexicons/pulled/`** — Downloaded via `lex-cli pull` from the atproto registry. 9 - Generated from the `pull.sources` list in `lex.config.js`. 10 - - **`lexicons/generated/`** — Auto-generated by `pnpm generate` from your Contrail config. 11 - Don't edit by hand. 12 - 13 - All three are picked up by `lex-cli generate` (see `lex.config.js` `files` glob).
-5
apps/rsvp-atmo/lexicons/pulled/README.md
··· 1 - # lexicon sources 2 - 3 - this directory contains lexicon documents pulled from the following sources: 4 - 5 - - atproto (nsids: app.bsky.actor.profile, community.lexicon.calendar.event, community.lexicon.calendar.rsvp, community.lexicon.location.address, community.lexicon.location.fsq, community.lexicon.location.geo, community.lexicon.location.hthree)
+4 -4
packages/contrail/src/core/spaces/acl.ts
··· 40 40 isOwner(space, did) || member != null; 41 41 42 42 /** Space-level access check. 43 - * Membership = access. Any member can read and write; the app filters 44 - * writes it doesn't want on its own side. Delete keeps the owner/own-record 45 - * rule so a random member can't nuke other people's records. */ 43 + * Membership = access. Any member (including owner) can read and write. 44 + * Delete is scoped to the caller's own records — owners don't get a bypass. 45 + * A random member can't nuke other people's records, and neither can the 46 + * owner. To remove a non-author record, delete the whole space. */ 46 47 export function checkAccess(input: AclInput): AclResult { 47 48 if (!checkAppPolicy(input.space.appPolicy, input.clientId)) { 48 49 return { allow: false, reason: "app-not-allowed" }; ··· 55 56 } 56 57 57 58 if (input.op === "delete") { 58 - if (isOwner(input.space, input.callerDid)) return { allow: true }; 59 59 if (!hasMember(input.space, input.member, input.callerDid)) { 60 60 return { allow: false, reason: "not-member" }; 61 61 }
-27
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/getCursor.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.getCursor", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Get the current cursor position", 8 - "output": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "properties": { 13 - "time_us": { 14 - "type": "integer" 15 - }, 16 - "date": { 17 - "type": "string" 18 - }, 19 - "seconds_ago": { 20 - "type": "integer" 21 - } 22 - } 23 - } 24 - } 25 - } 26 - } 27 - }
-51
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/getOverview.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.getOverview", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Get an overview of all indexed collections", 8 - "output": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "total_records", 14 - "collections" 15 - ], 16 - "properties": { 17 - "total_records": { 18 - "type": "integer" 19 - }, 20 - "collections": { 21 - "type": "array", 22 - "items": { 23 - "type": "ref", 24 - "ref": "#collectionStats" 25 - } 26 - } 27 - } 28 - } 29 - } 30 - }, 31 - "collectionStats": { 32 - "type": "object", 33 - "required": [ 34 - "collection", 35 - "records", 36 - "unique_users" 37 - ], 38 - "properties": { 39 - "collection": { 40 - "type": "string" 41 - }, 42 - "records": { 43 - "type": "integer" 44 - }, 45 - "unique_users": { 46 - "type": "integer" 47 - } 48 - } 49 - } 50 - } 51 - }
-138
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/getProfile.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.getProfile", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Get a user's profiles by DID or handle", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "actor" 12 - ], 13 - "properties": { 14 - "actor": { 15 - "type": "string", 16 - "format": "at-identifier", 17 - "description": "DID or handle of the user" 18 - } 19 - } 20 - }, 21 - "output": { 22 - "encoding": "application/json", 23 - "schema": { 24 - "type": "object", 25 - "required": [ 26 - "profiles" 27 - ], 28 - "properties": { 29 - "profiles": { 30 - "type": "array", 31 - "items": { 32 - "type": "ref", 33 - "ref": "#profileEntry" 34 - } 35 - } 36 - } 37 - } 38 - } 39 - }, 40 - "profileEntry": { 41 - "type": "object", 42 - "required": [ 43 - "did" 44 - ], 45 - "properties": { 46 - "did": { 47 - "type": "string", 48 - "format": "did" 49 - }, 50 - "handle": { 51 - "type": "string" 52 - }, 53 - "uri": { 54 - "type": "string", 55 - "format": "at-uri" 56 - }, 57 - "cid": { 58 - "type": "string", 59 - "format": "cid" 60 - }, 61 - "value": { 62 - "type": "ref", 63 - "ref": "#appBskyActorProfile" 64 - }, 65 - "collection": { 66 - "type": "string", 67 - "format": "nsid" 68 - }, 69 - "rkey": { 70 - "type": "string" 71 - } 72 - } 73 - }, 74 - "appBskyActorProfile": { 75 - "type": "object", 76 - "properties": { 77 - "avatar": { 78 - "type": "blob", 79 - "accept": [ 80 - "image/png", 81 - "image/jpeg" 82 - ], 83 - "maxSize": 1000000, 84 - "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 85 - }, 86 - "banner": { 87 - "type": "blob", 88 - "accept": [ 89 - "image/png", 90 - "image/jpeg" 91 - ], 92 - "maxSize": 1000000, 93 - "description": "Larger horizontal image to display behind profile view." 94 - }, 95 - "labels": { 96 - "refs": [ 97 - "com.atproto.label.defs#selfLabels" 98 - ], 99 - "type": "union", 100 - "description": "Self-label values, specific to the Bluesky application, on the overall account." 101 - }, 102 - "website": { 103 - "type": "string", 104 - "format": "uri" 105 - }, 106 - "pronouns": { 107 - "type": "string", 108 - "maxLength": 200, 109 - "description": "Free-form pronouns text.", 110 - "maxGraphemes": 20 111 - }, 112 - "createdAt": { 113 - "type": "string", 114 - "format": "datetime" 115 - }, 116 - "pinnedPost": { 117 - "ref": "com.atproto.repo.strongRef", 118 - "type": "ref" 119 - }, 120 - "description": { 121 - "type": "string", 122 - "maxLength": 2560, 123 - "description": "Free-form profile description text.", 124 - "maxGraphemes": 256 125 - }, 126 - "displayName": { 127 - "type": "string", 128 - "maxLength": 640, 129 - "maxGraphemes": 64 130 - }, 131 - "joinedViaStarterPack": { 132 - "ref": "com.atproto.repo.strongRef", 133 - "type": "ref" 134 - } 135 - } 136 - } 137 - } 138 - }
-59
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/notifyOfUpdate.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.notifyOfUpdate", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Notify of a record change for immediate indexing. Fetches the record from the user's PDS and indexes (or deletes) it.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "properties": { 13 - "uri": { 14 - "type": "string", 15 - "format": "at-uri", 16 - "description": "Single AT URI to fetch and index" 17 - }, 18 - "uris": { 19 - "type": "array", 20 - "items": { 21 - "type": "string", 22 - "format": "at-uri" 23 - }, 24 - "maxLength": 25, 25 - "description": "Batch of AT URIs to fetch and index (max 25)" 26 - } 27 - } 28 - } 29 - }, 30 - "output": { 31 - "encoding": "application/json", 32 - "schema": { 33 - "type": "object", 34 - "required": [ 35 - "indexed", 36 - "deleted" 37 - ], 38 - "properties": { 39 - "indexed": { 40 - "type": "integer", 41 - "description": "Number of records created or updated" 42 - }, 43 - "deleted": { 44 - "type": "integer", 45 - "description": "Number of records deleted (not found on PDS)" 46 - }, 47 - "errors": { 48 - "type": "array", 49 - "items": { 50 - "type": "string" 51 - }, 52 - "description": "Errors for individual URIs that could not be processed" 53 - } 54 - } 55 - } 56 - } 57 - } 58 - } 59 - }
-64
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/permissionSet.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.permissionSet", 4 - "defs": { 5 - "main": { 6 - "type": "permission-set", 7 - "title": "rsvp.atmo", 8 - "description": "All XRPC methods exposed by the rsvp.atmo service.", 9 - "permissions": [ 10 - { 11 - "type": "permission", 12 - "resource": "rpc", 13 - "aud": "*", 14 - "lxm": [ 15 - "rsvp.atmo.community.adopt", 16 - "rsvp.atmo.community.delete", 17 - "rsvp.atmo.community.deleteRecord", 18 - "rsvp.atmo.community.getHealth", 19 - "rsvp.atmo.community.list", 20 - "rsvp.atmo.community.mint", 21 - "rsvp.atmo.community.putRecord", 22 - "rsvp.atmo.community.reauth", 23 - "rsvp.atmo.community.space.create", 24 - "rsvp.atmo.community.space.delete", 25 - "rsvp.atmo.community.space.deleteRecord", 26 - "rsvp.atmo.community.space.grant", 27 - "rsvp.atmo.community.space.listMembers", 28 - "rsvp.atmo.community.space.putRecord", 29 - "rsvp.atmo.community.space.resync", 30 - "rsvp.atmo.community.space.revoke", 31 - "rsvp.atmo.community.space.setAccessLevel", 32 - "rsvp.atmo.event.getRecord", 33 - "rsvp.atmo.event.listRecords", 34 - "rsvp.atmo.getCursor", 35 - "rsvp.atmo.getOverview", 36 - "rsvp.atmo.getProfile", 37 - "rsvp.atmo.invite.create", 38 - "rsvp.atmo.invite.list", 39 - "rsvp.atmo.invite.redeem", 40 - "rsvp.atmo.invite.revoke", 41 - "rsvp.atmo.notifyOfUpdate", 42 - "rsvp.atmo.rsvp.getRecord", 43 - "rsvp.atmo.rsvp.listRecords", 44 - "rsvp.atmo.space.addMember", 45 - "rsvp.atmo.space.createSpace", 46 - "rsvp.atmo.space.deleteRecord", 47 - "rsvp.atmo.space.getBlob", 48 - "rsvp.atmo.space.getRecord", 49 - "rsvp.atmo.space.getSpace", 50 - "rsvp.atmo.space.leaveSpace", 51 - "rsvp.atmo.space.listBlobs", 52 - "rsvp.atmo.space.listMembers", 53 - "rsvp.atmo.space.listRecords", 54 - "rsvp.atmo.space.listSpaces", 55 - "rsvp.atmo.space.putRecord", 56 - "rsvp.atmo.space.removeMember", 57 - "rsvp.atmo.space.uploadBlob", 58 - "rsvp.atmo.spaceExt.whoami" 59 - ] 60 - } 61 - ] 62 - } 63 - } 64 - }
-56
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/adopt.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.adopt", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Adopt an existing ATProto account as a community identity. The provided app password is verified by creating a session, then stored encrypted. Creates the reserved `$admin` and `$publishers` spaces with the caller as `owner` in both.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "identifier", 14 - "appPassword" 15 - ], 16 - "properties": { 17 - "identifier": { 18 - "type": "string", 19 - "description": "Handle or DID of the account to adopt." 20 - }, 21 - "appPassword": { 22 - "type": "string", 23 - "description": "App password for the account; verified by createSession and stored encrypted." 24 - } 25 - } 26 - } 27 - }, 28 - "output": { 29 - "encoding": "application/json", 30 - "schema": { 31 - "type": "object", 32 - "required": [ 33 - "communityDid" 34 - ], 35 - "properties": { 36 - "communityDid": { 37 - "type": "string", 38 - "format": "did" 39 - } 40 - } 41 - } 42 - }, 43 - "errors": [ 44 - { 45 - "name": "InvalidRequest" 46 - }, 47 - { 48 - "name": "Unauthorized" 49 - }, 50 - { 51 - "name": "AlreadyExists" 52 - } 53 - ] 54 - } 55 - } 56 - }
-185
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/defs.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.defs", 4 - "description": "Shared types for community-module XRPC methods.", 5 - "defs": { 6 - "accessLevel": { 7 - "type": "string", 8 - "knownValues": [ 9 - "member", 10 - "manager", 11 - "admin", 12 - "owner" 13 - ], 14 - "description": "Module-internal access levels that govern community operations on a given space. Ordered: member < manager < admin < owner." 15 - }, 16 - "communityView": { 17 - "type": "object", 18 - "required": [ 19 - "did", 20 - "mode", 21 - "createdAt" 22 - ], 23 - "properties": { 24 - "did": { 25 - "type": "string", 26 - "format": "did" 27 - }, 28 - "mode": { 29 - "type": "string", 30 - "knownValues": [ 31 - "adopt", 32 - "mint" 33 - ] 34 - }, 35 - "identifier": { 36 - "type": "string", 37 - "description": "Handle or DID at adoption time (adopt only)." 38 - }, 39 - "createdAt": { 40 - "type": "integer" 41 - } 42 - } 43 - }, 44 - "spaceView": { 45 - "type": "object", 46 - "required": [ 47 - "uri", 48 - "ownerDid", 49 - "type", 50 - "key", 51 - "serviceDid", 52 - "createdAt" 53 - ], 54 - "properties": { 55 - "uri": { 56 - "type": "string", 57 - "format": "at-uri" 58 - }, 59 - "ownerDid": { 60 - "type": "string", 61 - "format": "did" 62 - }, 63 - "type": { 64 - "type": "string", 65 - "format": "nsid" 66 - }, 67 - "key": { 68 - "type": "string" 69 - }, 70 - "serviceDid": { 71 - "type": "string" 72 - }, 73 - "createdAt": { 74 - "type": "integer" 75 - } 76 - } 77 - }, 78 - "subject": { 79 - "type": "object", 80 - "description": "Exactly one of `did` or `spaceUri` must be set.", 81 - "properties": { 82 - "did": { 83 - "type": "string", 84 - "format": "did" 85 - }, 86 - "spaceUri": { 87 - "type": "string", 88 - "format": "at-uri" 89 - } 90 - } 91 - }, 92 - "memberRow": { 93 - "type": "object", 94 - "required": [ 95 - "subject", 96 - "accessLevel", 97 - "grantedBy", 98 - "grantedAt" 99 - ], 100 - "properties": { 101 - "subject": { 102 - "type": "ref", 103 - "ref": "#subject" 104 - }, 105 - "accessLevel": { 106 - "type": "ref", 107 - "ref": "#accessLevel" 108 - }, 109 - "grantedBy": { 110 - "type": "string", 111 - "format": "did" 112 - }, 113 - "grantedAt": { 114 - "type": "integer" 115 - } 116 - } 117 - }, 118 - "flatMember": { 119 - "type": "object", 120 - "required": [ 121 - "did", 122 - "addedAt" 123 - ], 124 - "properties": { 125 - "did": { 126 - "type": "string", 127 - "format": "did" 128 - }, 129 - "addedAt": { 130 - "type": "integer" 131 - } 132 - } 133 - }, 134 - "inviteView": { 135 - "type": "object", 136 - "required": [ 137 - "tokenHash", 138 - "spaceUri", 139 - "accessLevel", 140 - "createdBy", 141 - "createdAt", 142 - "usedCount" 143 - ], 144 - "properties": { 145 - "tokenHash": { 146 - "type": "string", 147 - "description": "SHA-256 of the raw token. Stable id for list/revoke; never grants access on its own." 148 - }, 149 - "spaceUri": { 150 - "type": "string", 151 - "format": "at-uri" 152 - }, 153 - "accessLevel": { 154 - "type": "ref", 155 - "ref": "#accessLevel" 156 - }, 157 - "createdBy": { 158 - "type": "string", 159 - "format": "did" 160 - }, 161 - "createdAt": { 162 - "type": "integer" 163 - }, 164 - "expiresAt": { 165 - "type": "integer", 166 - "description": "Unix ms. Omitted for no expiry." 167 - }, 168 - "maxUses": { 169 - "type": "integer", 170 - "description": "Omitted for unlimited." 171 - }, 172 - "usedCount": { 173 - "type": "integer" 174 - }, 175 - "revokedAt": { 176 - "type": "integer", 177 - "description": "Unix ms. Omitted if not revoked." 178 - }, 179 - "note": { 180 - "type": "string" 181 - } 182 - } 183 - } 184 - } 185 - }
-47
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/delete.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.delete", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Soft-delete a community. Also soft-deletes all spaces owned by the community. Caller must have `owner` in the community's `$admin` space.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "communityDid" 14 - ], 15 - "properties": { 16 - "communityDid": { 17 - "type": "string", 18 - "format": "did" 19 - } 20 - } 21 - } 22 - }, 23 - "output": { 24 - "encoding": "application/json", 25 - "schema": { 26 - "type": "object", 27 - "required": [ 28 - "ok" 29 - ], 30 - "properties": { 31 - "ok": { 32 - "type": "boolean" 33 - } 34 - } 35 - } 36 - }, 37 - "errors": [ 38 - { 39 - "name": "NotFound" 40 - }, 41 - { 42 - "name": "Forbidden" 43 - } 44 - ] 45 - } 46 - } 47 - }
-65
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/deleteRecord.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.deleteRecord", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Delete a public record authored by the community from the community's PDS. Adopted communities only. Caller must be `member` or higher in the community's `$publishers` space.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "communityDid", 14 - "collection", 15 - "rkey" 16 - ], 17 - "properties": { 18 - "communityDid": { 19 - "type": "string", 20 - "format": "did" 21 - }, 22 - "collection": { 23 - "type": "string", 24 - "format": "nsid" 25 - }, 26 - "rkey": { 27 - "type": "string" 28 - } 29 - } 30 - } 31 - }, 32 - "output": { 33 - "encoding": "application/json", 34 - "schema": { 35 - "type": "object", 36 - "required": [ 37 - "ok" 38 - ], 39 - "properties": { 40 - "ok": { 41 - "type": "boolean" 42 - } 43 - } 44 - } 45 - }, 46 - "errors": [ 47 - { 48 - "name": "InvalidRequest" 49 - }, 50 - { 51 - "name": "NotFound" 52 - }, 53 - { 54 - "name": "Forbidden" 55 - }, 56 - { 57 - "name": "NotSupported" 58 - }, 59 - { 60 - "name": "UpstreamFailure" 61 - } 62 - ] 63 - } 64 - } 65 - }
-49
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/getHealth.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.getHealth", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Check whether stored credentials for the community are still usable. Adopted: attempts a live session creation. Minted: verifies the signing key can be decrypted. Caller must have at least `member` in the community's `$admin` space.", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "communityDid" 12 - ], 13 - "properties": { 14 - "communityDid": { 15 - "type": "string", 16 - "format": "did" 17 - } 18 - } 19 - }, 20 - "output": { 21 - "encoding": "application/json", 22 - "schema": { 23 - "type": "object", 24 - "required": [ 25 - "status" 26 - ], 27 - "properties": { 28 - "status": { 29 - "type": "string", 30 - "knownValues": [ 31 - "healthy", 32 - "degraded", 33 - "expired" 34 - ] 35 - } 36 - } 37 - } 38 - }, 39 - "errors": [ 40 - { 41 - "name": "NotFound" 42 - }, 43 - { 44 - "name": "Forbidden" 45 - } 46 - ] 47 - } 48 - } 49 - }
-38
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/list.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.list", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "List communities where the actor has any access level in any community-owned space. Defaults to the JWT issuer when `actor` is omitted.", 8 - "parameters": { 9 - "type": "params", 10 - "properties": { 11 - "actor": { 12 - "type": "string", 13 - "format": "did", 14 - "description": "DID to query (defaults to the JWT issuer)." 15 - } 16 - } 17 - }, 18 - "output": { 19 - "encoding": "application/json", 20 - "schema": { 21 - "type": "object", 22 - "required": [ 23 - "communities" 24 - ], 25 - "properties": { 26 - "communities": { 27 - "type": "array", 28 - "items": { 29 - "type": "ref", 30 - "ref": "rsvp.atmo.community.defs#communityView" 31 - } 32 - } 33 - } 34 - } 35 - } 36 - } 37 - } 38 - }
-51
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/mint.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.mint", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Mint a fresh did:plc for a new community. Contrail generates three P-256 keypairs (signing, contrail rotation, creator rotation), submits a genesis op to the PLC directory, and stores the signing + contrail-rotation keys encrypted. The creator's rotation key is returned once as a recovery secret and never stored.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "properties": { 13 - "handle": { 14 - "type": "string", 15 - "description": "Optional handle to include in alsoKnownAs (as at://<handle>)." 16 - }, 17 - "pdsEndpoint": { 18 - "type": "string", 19 - "description": "Optional PDS endpoint to include as the atproto_pds service." 20 - } 21 - } 22 - } 23 - }, 24 - "output": { 25 - "encoding": "application/json", 26 - "schema": { 27 - "type": "object", 28 - "required": [ 29 - "communityDid", 30 - "recoveryKey" 31 - ], 32 - "properties": { 33 - "communityDid": { 34 - "type": "string", 35 - "format": "did" 36 - }, 37 - "recoveryKey": { 38 - "type": "unknown", 39 - "description": "Creator's rotation key as a private JWK. Shown once — cannot be retrieved later." 40 - } 41 - } 42 - } 43 - }, 44 - "errors": [ 45 - { 46 - "name": "UpstreamFailure" 47 - } 48 - ] 49 - } 50 - } 51 - }
-73
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/putRecord.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.putRecord", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Publish a public record authored by the community via the community's PDS. Adopted communities only. Caller must be `member` or higher in the community's `$publishers` space.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "communityDid", 14 - "collection", 15 - "record" 16 - ], 17 - "properties": { 18 - "communityDid": { 19 - "type": "string", 20 - "format": "did" 21 - }, 22 - "collection": { 23 - "type": "string", 24 - "format": "nsid" 25 - }, 26 - "rkey": { 27 - "type": "string" 28 - }, 29 - "record": { 30 - "type": "unknown" 31 - }, 32 - "validate": { 33 - "type": "boolean" 34 - } 35 - } 36 - } 37 - }, 38 - "output": { 39 - "encoding": "application/json", 40 - "schema": { 41 - "type": "object", 42 - "properties": { 43 - "uri": { 44 - "type": "string", 45 - "format": "at-uri" 46 - }, 47 - "cid": { 48 - "type": "string", 49 - "format": "cid" 50 - } 51 - } 52 - } 53 - }, 54 - "errors": [ 55 - { 56 - "name": "InvalidRequest" 57 - }, 58 - { 59 - "name": "NotFound" 60 - }, 61 - { 62 - "name": "Forbidden" 63 - }, 64 - { 65 - "name": "NotSupported" 66 - }, 67 - { 68 - "name": "UpstreamFailure" 69 - } 70 - ] 71 - } 72 - } 73 - }
-60
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/reauth.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.reauth", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Replace the stored app password for an adopted community. Caller must have `owner` access in the community's `$admin` space.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "communityDid", 14 - "appPassword" 15 - ], 16 - "properties": { 17 - "communityDid": { 18 - "type": "string", 19 - "format": "did" 20 - }, 21 - "appPassword": { 22 - "type": "string" 23 - } 24 - } 25 - } 26 - }, 27 - "output": { 28 - "encoding": "application/json", 29 - "schema": { 30 - "type": "object", 31 - "required": [ 32 - "ok" 33 - ], 34 - "properties": { 35 - "ok": { 36 - "type": "boolean" 37 - } 38 - } 39 - } 40 - }, 41 - "errors": [ 42 - { 43 - "name": "InvalidRequest" 44 - }, 45 - { 46 - "name": "NotFound" 47 - }, 48 - { 49 - "name": "Forbidden" 50 - }, 51 - { 52 - "name": "Unauthorized" 53 - }, 54 - { 55 - "name": "NotSupported" 56 - } 57 - ] 58 - } 59 - } 60 - }
-284
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/event/getRecord.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.event.getRecord", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Get a single community.lexicon.calendar.event record by AT URI", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "uri" 12 - ], 13 - "properties": { 14 - "uri": { 15 - "type": "string", 16 - "format": "at-uri", 17 - "description": "AT URI of the record" 18 - }, 19 - "profiles": { 20 - "type": "boolean", 21 - "description": "Include profile + identity info keyed by DID" 22 - }, 23 - "spaceUri": { 24 - "type": "string", 25 - "format": "at-uri", 26 - "description": "If set, fetch from this permissioned space (requires service-auth JWT or a read-grant invite token)." 27 - }, 28 - "inviteToken": { 29 - "type": "string", 30 - "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." 31 - }, 32 - "hydrateRsvps": { 33 - "type": "integer", 34 - "minimum": 1, 35 - "maximum": 50, 36 - "description": "Number of rsvps records to embed" 37 - } 38 - } 39 - }, 40 - "output": { 41 - "encoding": "application/json", 42 - "schema": { 43 - "type": "object", 44 - "required": [ 45 - "uri", 46 - "value" 47 - ], 48 - "properties": { 49 - "uri": { 50 - "type": "string", 51 - "format": "at-uri" 52 - }, 53 - "cid": { 54 - "type": "string", 55 - "format": "cid" 56 - }, 57 - "value": { 58 - "type": "ref", 59 - "ref": "community.lexicon.calendar.event#main" 60 - }, 61 - "did": { 62 - "type": "string", 63 - "format": "did" 64 - }, 65 - "collection": { 66 - "type": "string", 67 - "format": "nsid" 68 - }, 69 - "rkey": { 70 - "type": "string" 71 - }, 72 - "time_us": { 73 - "type": "integer" 74 - }, 75 - "space": { 76 - "type": "string", 77 - "format": "at-uri", 78 - "description": "Present when the record was read from a permissioned space; its value is the space URI." 79 - }, 80 - "rsvpsCount": { 81 - "type": "integer", 82 - "description": "Total rsvps count" 83 - }, 84 - "rsvpsInterestedCount": { 85 - "type": "integer", 86 - "description": "rsvps count where status = interested" 87 - }, 88 - "rsvpsGoingCount": { 89 - "type": "integer", 90 - "description": "rsvps count where status = going" 91 - }, 92 - "rsvpsNotgoingCount": { 93 - "type": "integer", 94 - "description": "rsvps count where status = notgoing" 95 - }, 96 - "rsvps": { 97 - "type": "ref", 98 - "ref": "#hydrateRsvps" 99 - }, 100 - "profiles": { 101 - "type": "array", 102 - "items": { 103 - "type": "ref", 104 - "ref": "#profileEntry" 105 - } 106 - } 107 - } 108 - } 109 - } 110 - }, 111 - "hydrateRsvpsRecord": { 112 - "type": "object", 113 - "required": [ 114 - "uri", 115 - "did", 116 - "collection", 117 - "rkey", 118 - "time_us" 119 - ], 120 - "properties": { 121 - "uri": { 122 - "type": "string", 123 - "format": "at-uri" 124 - }, 125 - "did": { 126 - "type": "string", 127 - "format": "did" 128 - }, 129 - "collection": { 130 - "type": "string", 131 - "format": "nsid" 132 - }, 133 - "rkey": { 134 - "type": "string" 135 - }, 136 - "cid": { 137 - "type": "string" 138 - }, 139 - "record": { 140 - "type": "ref", 141 - "ref": "community.lexicon.calendar.rsvp#main" 142 - }, 143 - "time_us": { 144 - "type": "integer" 145 - }, 146 - "space": { 147 - "type": "string", 148 - "format": "at-uri", 149 - "description": "Present when the record was read from a permissioned space." 150 - } 151 - } 152 - }, 153 - "hydrateRsvps": { 154 - "type": "object", 155 - "properties": { 156 - "interested": { 157 - "type": "array", 158 - "items": { 159 - "type": "ref", 160 - "ref": "#hydrateRsvpsRecord" 161 - } 162 - }, 163 - "going": { 164 - "type": "array", 165 - "items": { 166 - "type": "ref", 167 - "ref": "#hydrateRsvpsRecord" 168 - } 169 - }, 170 - "notgoing": { 171 - "type": "array", 172 - "items": { 173 - "type": "ref", 174 - "ref": "#hydrateRsvpsRecord" 175 - } 176 - }, 177 - "other": { 178 - "type": "array", 179 - "items": { 180 - "type": "ref", 181 - "ref": "#hydrateRsvpsRecord" 182 - } 183 - } 184 - } 185 - }, 186 - "profileEntry": { 187 - "type": "object", 188 - "required": [ 189 - "did" 190 - ], 191 - "properties": { 192 - "did": { 193 - "type": "string", 194 - "format": "did" 195 - }, 196 - "handle": { 197 - "type": "string" 198 - }, 199 - "uri": { 200 - "type": "string", 201 - "format": "at-uri" 202 - }, 203 - "cid": { 204 - "type": "string", 205 - "format": "cid" 206 - }, 207 - "value": { 208 - "type": "ref", 209 - "ref": "#appBskyActorProfile" 210 - }, 211 - "collection": { 212 - "type": "string", 213 - "format": "nsid" 214 - }, 215 - "rkey": { 216 - "type": "string" 217 - } 218 - } 219 - }, 220 - "appBskyActorProfile": { 221 - "type": "object", 222 - "properties": { 223 - "avatar": { 224 - "type": "blob", 225 - "accept": [ 226 - "image/png", 227 - "image/jpeg" 228 - ], 229 - "maxSize": 1000000, 230 - "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 231 - }, 232 - "banner": { 233 - "type": "blob", 234 - "accept": [ 235 - "image/png", 236 - "image/jpeg" 237 - ], 238 - "maxSize": 1000000, 239 - "description": "Larger horizontal image to display behind profile view." 240 - }, 241 - "labels": { 242 - "refs": [ 243 - "com.atproto.label.defs#selfLabels" 244 - ], 245 - "type": "union", 246 - "description": "Self-label values, specific to the Bluesky application, on the overall account." 247 - }, 248 - "website": { 249 - "type": "string", 250 - "format": "uri" 251 - }, 252 - "pronouns": { 253 - "type": "string", 254 - "maxLength": 200, 255 - "description": "Free-form pronouns text.", 256 - "maxGraphemes": 20 257 - }, 258 - "createdAt": { 259 - "type": "string", 260 - "format": "datetime" 261 - }, 262 - "pinnedPost": { 263 - "ref": "com.atproto.repo.strongRef", 264 - "type": "ref" 265 - }, 266 - "description": { 267 - "type": "string", 268 - "maxLength": 2560, 269 - "description": "Free-form profile description text.", 270 - "maxGraphemes": 256 271 - }, 272 - "displayName": { 273 - "type": "string", 274 - "maxLength": 640, 275 - "maxGraphemes": 64 276 - }, 277 - "joinedViaStarterPack": { 278 - "ref": "com.atproto.repo.strongRef", 279 - "type": "ref" 280 - } 281 - } 282 - } 283 - } 284 - }
-394
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/event/listRecords.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.event.listRecords", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Query community.lexicon.calendar.event records with filters", 8 - "parameters": { 9 - "type": "params", 10 - "properties": { 11 - "limit": { 12 - "type": "integer", 13 - "minimum": 1, 14 - "maximum": 200, 15 - "default": 50 16 - }, 17 - "cursor": { 18 - "type": "string" 19 - }, 20 - "actor": { 21 - "type": "string", 22 - "format": "at-identifier", 23 - "description": "Filter by DID or handle (triggers on-demand backfill)" 24 - }, 25 - "profiles": { 26 - "type": "boolean", 27 - "description": "Include profile + identity info keyed by DID" 28 - }, 29 - "spaceUri": { 30 - "type": "string", 31 - "format": "at-uri", 32 - "description": "If set, query records inside this permissioned space (requires service-auth JWT or a read-grant invite token)." 33 - }, 34 - "byUser": { 35 - "type": "string", 36 - "format": "did", 37 - "description": "Only used with spaceUri — filter to records authored by this DID." 38 - }, 39 - "inviteToken": { 40 - "type": "string", 41 - "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." 42 - }, 43 - "search": { 44 - "type": "string", 45 - "description": "Full-text search across: name, description" 46 - }, 47 - "mode": { 48 - "type": "string", 49 - "description": "Filter by mode" 50 - }, 51 - "name": { 52 - "type": "string", 53 - "description": "Filter by name" 54 - }, 55 - "status": { 56 - "type": "string", 57 - "description": "Filter by status" 58 - }, 59 - "startsAtMin": { 60 - "type": "string", 61 - "description": "Minimum value for startsAt" 62 - }, 63 - "startsAtMax": { 64 - "type": "string", 65 - "description": "Maximum value for startsAt" 66 - }, 67 - "endsAtMin": { 68 - "type": "string", 69 - "description": "Minimum value for endsAt" 70 - }, 71 - "endsAtMax": { 72 - "type": "string", 73 - "description": "Maximum value for endsAt" 74 - }, 75 - "createdAtMin": { 76 - "type": "string", 77 - "description": "Minimum value for createdAt" 78 - }, 79 - "createdAtMax": { 80 - "type": "string", 81 - "description": "Maximum value for createdAt" 82 - }, 83 - "rsvpsCountMin": { 84 - "type": "integer", 85 - "description": "Minimum total rsvps count" 86 - }, 87 - "hydrateRsvps": { 88 - "type": "integer", 89 - "minimum": 1, 90 - "maximum": 50, 91 - "description": "Number of rsvps records to embed per record" 92 - }, 93 - "rsvpsInterestedCountMin": { 94 - "type": "integer", 95 - "description": "Minimum rsvps count where status = interested" 96 - }, 97 - "rsvpsGoingCountMin": { 98 - "type": "integer", 99 - "description": "Minimum rsvps count where status = going" 100 - }, 101 - "rsvpsNotgoingCountMin": { 102 - "type": "integer", 103 - "description": "Minimum rsvps count where status = notgoing" 104 - }, 105 - "sort": { 106 - "type": "string", 107 - "knownValues": [ 108 - "mode", 109 - "name", 110 - "status", 111 - "startsAt", 112 - "endsAt", 113 - "createdAt", 114 - "rsvpsCount", 115 - "rsvpsInterestedCount", 116 - "rsvpsGoingCount", 117 - "rsvpsNotgoingCount" 118 - ], 119 - "description": "Field to sort by (default: time_us)" 120 - }, 121 - "order": { 122 - "type": "string", 123 - "knownValues": [ 124 - "asc", 125 - "desc" 126 - ], 127 - "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" 128 - } 129 - } 130 - }, 131 - "output": { 132 - "encoding": "application/json", 133 - "schema": { 134 - "type": "object", 135 - "required": [ 136 - "records" 137 - ], 138 - "properties": { 139 - "records": { 140 - "type": "array", 141 - "items": { 142 - "type": "ref", 143 - "ref": "#record" 144 - } 145 - }, 146 - "cursor": { 147 - "type": "string" 148 - }, 149 - "profiles": { 150 - "type": "array", 151 - "items": { 152 - "type": "ref", 153 - "ref": "#profileEntry" 154 - } 155 - } 156 - } 157 - } 158 - } 159 - }, 160 - "record": { 161 - "type": "object", 162 - "required": [ 163 - "uri", 164 - "cid", 165 - "value" 166 - ], 167 - "properties": { 168 - "uri": { 169 - "type": "string", 170 - "format": "at-uri" 171 - }, 172 - "cid": { 173 - "type": "string", 174 - "format": "cid" 175 - }, 176 - "value": { 177 - "type": "ref", 178 - "ref": "community.lexicon.calendar.event#main" 179 - }, 180 - "did": { 181 - "type": "string", 182 - "format": "did" 183 - }, 184 - "collection": { 185 - "type": "string", 186 - "format": "nsid" 187 - }, 188 - "rkey": { 189 - "type": "string" 190 - }, 191 - "time_us": { 192 - "type": "integer" 193 - }, 194 - "space": { 195 - "type": "string", 196 - "format": "at-uri", 197 - "description": "Present when the record was read from a permissioned space; its value is the space URI." 198 - }, 199 - "rsvpsCount": { 200 - "type": "integer", 201 - "description": "Total rsvps count" 202 - }, 203 - "rsvpsInterestedCount": { 204 - "type": "integer", 205 - "description": "rsvps count where status = interested" 206 - }, 207 - "rsvpsGoingCount": { 208 - "type": "integer", 209 - "description": "rsvps count where status = going" 210 - }, 211 - "rsvpsNotgoingCount": { 212 - "type": "integer", 213 - "description": "rsvps count where status = notgoing" 214 - }, 215 - "rsvps": { 216 - "type": "ref", 217 - "ref": "#hydrateRsvps" 218 - } 219 - } 220 - }, 221 - "hydrateRsvpsRecord": { 222 - "type": "object", 223 - "required": [ 224 - "uri", 225 - "did", 226 - "collection", 227 - "rkey", 228 - "time_us" 229 - ], 230 - "properties": { 231 - "uri": { 232 - "type": "string", 233 - "format": "at-uri" 234 - }, 235 - "did": { 236 - "type": "string", 237 - "format": "did" 238 - }, 239 - "collection": { 240 - "type": "string", 241 - "format": "nsid" 242 - }, 243 - "rkey": { 244 - "type": "string" 245 - }, 246 - "cid": { 247 - "type": "string" 248 - }, 249 - "record": { 250 - "type": "ref", 251 - "ref": "community.lexicon.calendar.rsvp#main" 252 - }, 253 - "time_us": { 254 - "type": "integer" 255 - }, 256 - "space": { 257 - "type": "string", 258 - "format": "at-uri", 259 - "description": "Present when the record was read from a permissioned space." 260 - } 261 - } 262 - }, 263 - "hydrateRsvps": { 264 - "type": "object", 265 - "properties": { 266 - "interested": { 267 - "type": "array", 268 - "items": { 269 - "type": "ref", 270 - "ref": "#hydrateRsvpsRecord" 271 - } 272 - }, 273 - "going": { 274 - "type": "array", 275 - "items": { 276 - "type": "ref", 277 - "ref": "#hydrateRsvpsRecord" 278 - } 279 - }, 280 - "notgoing": { 281 - "type": "array", 282 - "items": { 283 - "type": "ref", 284 - "ref": "#hydrateRsvpsRecord" 285 - } 286 - }, 287 - "other": { 288 - "type": "array", 289 - "items": { 290 - "type": "ref", 291 - "ref": "#hydrateRsvpsRecord" 292 - } 293 - } 294 - } 295 - }, 296 - "profileEntry": { 297 - "type": "object", 298 - "required": [ 299 - "did" 300 - ], 301 - "properties": { 302 - "did": { 303 - "type": "string", 304 - "format": "did" 305 - }, 306 - "handle": { 307 - "type": "string" 308 - }, 309 - "uri": { 310 - "type": "string", 311 - "format": "at-uri" 312 - }, 313 - "cid": { 314 - "type": "string", 315 - "format": "cid" 316 - }, 317 - "value": { 318 - "type": "ref", 319 - "ref": "#appBskyActorProfile" 320 - }, 321 - "collection": { 322 - "type": "string", 323 - "format": "nsid" 324 - }, 325 - "rkey": { 326 - "type": "string" 327 - } 328 - } 329 - }, 330 - "appBskyActorProfile": { 331 - "type": "object", 332 - "properties": { 333 - "avatar": { 334 - "type": "blob", 335 - "accept": [ 336 - "image/png", 337 - "image/jpeg" 338 - ], 339 - "maxSize": 1000000, 340 - "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 341 - }, 342 - "banner": { 343 - "type": "blob", 344 - "accept": [ 345 - "image/png", 346 - "image/jpeg" 347 - ], 348 - "maxSize": 1000000, 349 - "description": "Larger horizontal image to display behind profile view." 350 - }, 351 - "labels": { 352 - "refs": [ 353 - "com.atproto.label.defs#selfLabels" 354 - ], 355 - "type": "union", 356 - "description": "Self-label values, specific to the Bluesky application, on the overall account." 357 - }, 358 - "website": { 359 - "type": "string", 360 - "format": "uri" 361 - }, 362 - "pronouns": { 363 - "type": "string", 364 - "maxLength": 200, 365 - "description": "Free-form pronouns text.", 366 - "maxGraphemes": 20 367 - }, 368 - "createdAt": { 369 - "type": "string", 370 - "format": "datetime" 371 - }, 372 - "pinnedPost": { 373 - "ref": "com.atproto.repo.strongRef", 374 - "type": "ref" 375 - }, 376 - "description": { 377 - "type": "string", 378 - "maxLength": 2560, 379 - "description": "Free-form profile description text.", 380 - "maxGraphemes": 256 381 - }, 382 - "displayName": { 383 - "type": "string", 384 - "maxLength": 640, 385 - "maxGraphemes": 64 386 - }, 387 - "joinedViaStarterPack": { 388 - "ref": "com.atproto.repo.strongRef", 389 - "type": "ref" 390 - } 391 - } 392 - } 393 - } 394 - }
-87
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/create.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.invite.create", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Create an invite for a space. The service dispatches on space ownership: user-owned spaces take `kind` (default `join`); community-owned spaces take `accessLevel`. Exactly one of `kind` / `accessLevel` must be set. Returns the raw token once; only the hash is stored.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "spaceUri" 14 - ], 15 - "properties": { 16 - "spaceUri": { 17 - "type": "string", 18 - "format": "at-uri" 19 - }, 20 - "kind": { 21 - "type": "string", 22 - "knownValues": [ 23 - "join", 24 - "read", 25 - "read-join" 26 - ], 27 - "description": "For user-owned spaces. join: redeem to become a member. read: bearer-only read access, no membership. read-join: anonymous read + signed-in redeem to join." 28 - }, 29 - "accessLevel": { 30 - "type": "string", 31 - "knownValues": [ 32 - "member", 33 - "manager", 34 - "admin", 35 - "owner" 36 - ], 37 - "description": "For community-owned spaces. The access level granted on redemption — the creator's own level caps what they can grant." 38 - }, 39 - "expiresAt": { 40 - "type": "integer", 41 - "description": "Unix ms timestamp. Omit for no expiry." 42 - }, 43 - "maxUses": { 44 - "type": "integer", 45 - "minimum": 1 46 - }, 47 - "note": { 48 - "type": "string", 49 - "maxLength": 500 50 - } 51 - } 52 - } 53 - }, 54 - "output": { 55 - "encoding": "application/json", 56 - "schema": { 57 - "type": "object", 58 - "required": [ 59 - "token", 60 - "invite" 61 - ], 62 - "properties": { 63 - "token": { 64 - "type": "string", 65 - "description": "Raw token. Shown once — cannot be retrieved later." 66 - }, 67 - "invite": { 68 - "type": "ref", 69 - "ref": "rsvp.atmo.invite.defs#inviteView" 70 - } 71 - } 72 - } 73 - }, 74 - "errors": [ 75 - { 76 - "name": "NotFound" 77 - }, 78 - { 79 - "name": "Forbidden" 80 - }, 81 - { 82 - "name": "InvalidRequest" 83 - } 84 - ] 85 - } 86 - } 87 - }
-73
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/defs.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.invite.defs", 4 - "defs": { 5 - "inviteView": { 6 - "type": "object", 7 - "description": "An invite row as exposed to clients. Either `kind` (user-owned space) or `accessLevel` (community-owned space) is set, never both.", 8 - "required": [ 9 - "tokenHash", 10 - "spaceUri", 11 - "createdBy", 12 - "createdAt", 13 - "usedCount" 14 - ], 15 - "properties": { 16 - "tokenHash": { 17 - "type": "string", 18 - "description": "Stable identifier for list/revoke operations." 19 - }, 20 - "spaceUri": { 21 - "type": "string", 22 - "format": "at-uri" 23 - }, 24 - "kind": { 25 - "type": "string", 26 - "knownValues": [ 27 - "join", 28 - "read", 29 - "read-join" 30 - ], 31 - "description": "Set for user-owned spaces. Absent for community-owned." 32 - }, 33 - "accessLevel": { 34 - "type": "string", 35 - "knownValues": [ 36 - "member", 37 - "manager", 38 - "admin", 39 - "owner" 40 - ], 41 - "description": "Set for community-owned spaces. Absent for user-owned." 42 - }, 43 - "createdBy": { 44 - "type": "string", 45 - "format": "did" 46 - }, 47 - "createdAt": { 48 - "type": "integer", 49 - "description": "Unix ms." 50 - }, 51 - "expiresAt": { 52 - "type": "integer", 53 - "description": "Unix ms. Omitted for no expiry." 54 - }, 55 - "maxUses": { 56 - "type": "integer", 57 - "minimum": 1 58 - }, 59 - "usedCount": { 60 - "type": "integer" 61 - }, 62 - "revokedAt": { 63 - "type": "integer", 64 - "description": "Unix ms. Omitted if not revoked." 65 - }, 66 - "note": { 67 - "type": "string", 68 - "maxLength": 500 69 - } 70 - } 71 - } 72 - } 73 - }
-52
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/list.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.invite.list", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "List invites for a space. User-owned spaces: owner-only. Community-owned spaces: manager+.", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "spaceUri" 12 - ], 13 - "properties": { 14 - "spaceUri": { 15 - "type": "string", 16 - "format": "at-uri" 17 - }, 18 - "includeRevoked": { 19 - "type": "boolean", 20 - "default": false 21 - } 22 - } 23 - }, 24 - "output": { 25 - "encoding": "application/json", 26 - "schema": { 27 - "type": "object", 28 - "required": [ 29 - "invites" 30 - ], 31 - "properties": { 32 - "invites": { 33 - "type": "array", 34 - "items": { 35 - "type": "ref", 36 - "ref": "rsvp.atmo.invite.defs#inviteView" 37 - } 38 - } 39 - } 40 - } 41 - }, 42 - "errors": [ 43 - { 44 - "name": "NotFound" 45 - }, 46 - { 47 - "name": "Forbidden" 48 - } 49 - ] 50 - } 51 - } 52 - }
-60
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/redeem.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.invite.redeem", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Redeem an invite token. User-owned spaces: caller becomes a member. Community-owned spaces: caller is granted the invite's access level.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "token" 14 - ], 15 - "properties": { 16 - "token": { 17 - "type": "string" 18 - } 19 - } 20 - } 21 - }, 22 - "output": { 23 - "encoding": "application/json", 24 - "schema": { 25 - "type": "object", 26 - "required": [ 27 - "spaceUri" 28 - ], 29 - "properties": { 30 - "spaceUri": { 31 - "type": "string", 32 - "format": "at-uri" 33 - }, 34 - "kind": { 35 - "type": "string", 36 - "description": "Set for user-owned spaces — echoes the invite kind consumed." 37 - }, 38 - "accessLevel": { 39 - "type": "string", 40 - "description": "Set for community-owned spaces — the level granted." 41 - }, 42 - "communityDid": { 43 - "type": "string", 44 - "format": "did", 45 - "description": "Set for community-owned spaces." 46 - } 47 - } 48 - } 49 - }, 50 - "errors": [ 51 - { 52 - "name": "InvalidInvite" 53 - }, 54 - { 55 - "name": "NotFound" 56 - } 57 - ] 58 - } 59 - } 60 - }
-51
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/revoke.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.invite.revoke", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Revoke an invite by tokenHash. User-owned spaces: owner-only. Community-owned spaces: invite creator OR manager+ on the target space.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "tokenHash" 14 - ], 15 - "properties": { 16 - "spaceUri": { 17 - "type": "string", 18 - "format": "at-uri", 19 - "description": "Optional — ownership is inferred from the invite row; required for user-owned spaces for a sanity check." 20 - }, 21 - "tokenHash": { 22 - "type": "string" 23 - } 24 - } 25 - } 26 - }, 27 - "output": { 28 - "encoding": "application/json", 29 - "schema": { 30 - "type": "object", 31 - "required": [ 32 - "ok" 33 - ], 34 - "properties": { 35 - "ok": { 36 - "type": "boolean" 37 - } 38 - } 39 - } 40 - }, 41 - "errors": [ 42 - { 43 - "name": "NotFound" 44 - }, 45 - { 46 - "name": "Forbidden" 47 - } 48 - ] 49 - } 50 - } 51 - }
-233
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/rsvp/getRecord.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.rsvp.getRecord", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Get a single community.lexicon.calendar.rsvp record by AT URI", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "uri" 12 - ], 13 - "properties": { 14 - "uri": { 15 - "type": "string", 16 - "format": "at-uri", 17 - "description": "AT URI of the record" 18 - }, 19 - "profiles": { 20 - "type": "boolean", 21 - "description": "Include profile + identity info keyed by DID" 22 - }, 23 - "spaceUri": { 24 - "type": "string", 25 - "format": "at-uri", 26 - "description": "If set, fetch from this permissioned space (requires service-auth JWT or a read-grant invite token)." 27 - }, 28 - "inviteToken": { 29 - "type": "string", 30 - "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." 31 - }, 32 - "hydrateEvent": { 33 - "type": "boolean", 34 - "description": "Embed the referenced event record" 35 - } 36 - } 37 - }, 38 - "output": { 39 - "encoding": "application/json", 40 - "schema": { 41 - "type": "object", 42 - "required": [ 43 - "uri", 44 - "value" 45 - ], 46 - "properties": { 47 - "uri": { 48 - "type": "string", 49 - "format": "at-uri" 50 - }, 51 - "cid": { 52 - "type": "string", 53 - "format": "cid" 54 - }, 55 - "value": { 56 - "type": "ref", 57 - "ref": "community.lexicon.calendar.rsvp#main" 58 - }, 59 - "did": { 60 - "type": "string", 61 - "format": "did" 62 - }, 63 - "collection": { 64 - "type": "string", 65 - "format": "nsid" 66 - }, 67 - "rkey": { 68 - "type": "string" 69 - }, 70 - "time_us": { 71 - "type": "integer" 72 - }, 73 - "space": { 74 - "type": "string", 75 - "format": "at-uri", 76 - "description": "Present when the record was read from a permissioned space; its value is the space URI." 77 - }, 78 - "event": { 79 - "type": "ref", 80 - "ref": "#refEventRecord" 81 - }, 82 - "profiles": { 83 - "type": "array", 84 - "items": { 85 - "type": "ref", 86 - "ref": "#profileEntry" 87 - } 88 - } 89 - } 90 - } 91 - } 92 - }, 93 - "refEventRecord": { 94 - "type": "object", 95 - "required": [ 96 - "uri", 97 - "did", 98 - "collection", 99 - "rkey", 100 - "time_us" 101 - ], 102 - "properties": { 103 - "uri": { 104 - "type": "string", 105 - "format": "at-uri" 106 - }, 107 - "did": { 108 - "type": "string", 109 - "format": "did" 110 - }, 111 - "collection": { 112 - "type": "string", 113 - "format": "nsid" 114 - }, 115 - "rkey": { 116 - "type": "string" 117 - }, 118 - "cid": { 119 - "type": "string" 120 - }, 121 - "record": { 122 - "type": "ref", 123 - "ref": "community.lexicon.calendar.event#main" 124 - }, 125 - "time_us": { 126 - "type": "integer" 127 - }, 128 - "space": { 129 - "type": "string", 130 - "format": "at-uri", 131 - "description": "Present when the record was read from a permissioned space." 132 - } 133 - } 134 - }, 135 - "profileEntry": { 136 - "type": "object", 137 - "required": [ 138 - "did" 139 - ], 140 - "properties": { 141 - "did": { 142 - "type": "string", 143 - "format": "did" 144 - }, 145 - "handle": { 146 - "type": "string" 147 - }, 148 - "uri": { 149 - "type": "string", 150 - "format": "at-uri" 151 - }, 152 - "cid": { 153 - "type": "string", 154 - "format": "cid" 155 - }, 156 - "value": { 157 - "type": "ref", 158 - "ref": "#appBskyActorProfile" 159 - }, 160 - "collection": { 161 - "type": "string", 162 - "format": "nsid" 163 - }, 164 - "rkey": { 165 - "type": "string" 166 - } 167 - } 168 - }, 169 - "appBskyActorProfile": { 170 - "type": "object", 171 - "properties": { 172 - "avatar": { 173 - "type": "blob", 174 - "accept": [ 175 - "image/png", 176 - "image/jpeg" 177 - ], 178 - "maxSize": 1000000, 179 - "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 180 - }, 181 - "banner": { 182 - "type": "blob", 183 - "accept": [ 184 - "image/png", 185 - "image/jpeg" 186 - ], 187 - "maxSize": 1000000, 188 - "description": "Larger horizontal image to display behind profile view." 189 - }, 190 - "labels": { 191 - "refs": [ 192 - "com.atproto.label.defs#selfLabels" 193 - ], 194 - "type": "union", 195 - "description": "Self-label values, specific to the Bluesky application, on the overall account." 196 - }, 197 - "website": { 198 - "type": "string", 199 - "format": "uri" 200 - }, 201 - "pronouns": { 202 - "type": "string", 203 - "maxLength": 200, 204 - "description": "Free-form pronouns text.", 205 - "maxGraphemes": 20 206 - }, 207 - "createdAt": { 208 - "type": "string", 209 - "format": "datetime" 210 - }, 211 - "pinnedPost": { 212 - "ref": "com.atproto.repo.strongRef", 213 - "type": "ref" 214 - }, 215 - "description": { 216 - "type": "string", 217 - "maxLength": 2560, 218 - "description": "Free-form profile description text.", 219 - "maxGraphemes": 256 220 - }, 221 - "displayName": { 222 - "type": "string", 223 - "maxLength": 640, 224 - "maxGraphemes": 64 225 - }, 226 - "joinedViaStarterPack": { 227 - "ref": "com.atproto.repo.strongRef", 228 - "type": "ref" 229 - } 230 - } 231 - } 232 - } 233 - }
-287
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/rsvp/listRecords.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.rsvp.listRecords", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Query community.lexicon.calendar.rsvp records with filters", 8 - "parameters": { 9 - "type": "params", 10 - "properties": { 11 - "limit": { 12 - "type": "integer", 13 - "minimum": 1, 14 - "maximum": 200, 15 - "default": 50 16 - }, 17 - "cursor": { 18 - "type": "string" 19 - }, 20 - "actor": { 21 - "type": "string", 22 - "format": "at-identifier", 23 - "description": "Filter by DID or handle (triggers on-demand backfill)" 24 - }, 25 - "profiles": { 26 - "type": "boolean", 27 - "description": "Include profile + identity info keyed by DID" 28 - }, 29 - "spaceUri": { 30 - "type": "string", 31 - "format": "at-uri", 32 - "description": "If set, query records inside this permissioned space (requires service-auth JWT or a read-grant invite token)." 33 - }, 34 - "byUser": { 35 - "type": "string", 36 - "format": "did", 37 - "description": "Only used with spaceUri — filter to records authored by this DID." 38 - }, 39 - "inviteToken": { 40 - "type": "string", 41 - "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." 42 - }, 43 - "status": { 44 - "type": "string", 45 - "description": "Filter by status" 46 - }, 47 - "subjectUri": { 48 - "type": "string", 49 - "description": "Filter by subject.uri" 50 - }, 51 - "hydrateEvent": { 52 - "type": "boolean", 53 - "description": "Embed the referenced event record" 54 - }, 55 - "sort": { 56 - "type": "string", 57 - "knownValues": [ 58 - "status", 59 - "subjectUri" 60 - ], 61 - "description": "Field to sort by (default: time_us)" 62 - }, 63 - "order": { 64 - "type": "string", 65 - "knownValues": [ 66 - "asc", 67 - "desc" 68 - ], 69 - "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" 70 - } 71 - } 72 - }, 73 - "output": { 74 - "encoding": "application/json", 75 - "schema": { 76 - "type": "object", 77 - "required": [ 78 - "records" 79 - ], 80 - "properties": { 81 - "records": { 82 - "type": "array", 83 - "items": { 84 - "type": "ref", 85 - "ref": "#record" 86 - } 87 - }, 88 - "cursor": { 89 - "type": "string" 90 - }, 91 - "profiles": { 92 - "type": "array", 93 - "items": { 94 - "type": "ref", 95 - "ref": "#profileEntry" 96 - } 97 - } 98 - } 99 - } 100 - } 101 - }, 102 - "record": { 103 - "type": "object", 104 - "required": [ 105 - "uri", 106 - "cid", 107 - "value" 108 - ], 109 - "properties": { 110 - "uri": { 111 - "type": "string", 112 - "format": "at-uri" 113 - }, 114 - "cid": { 115 - "type": "string", 116 - "format": "cid" 117 - }, 118 - "value": { 119 - "type": "ref", 120 - "ref": "community.lexicon.calendar.rsvp#main" 121 - }, 122 - "did": { 123 - "type": "string", 124 - "format": "did" 125 - }, 126 - "collection": { 127 - "type": "string", 128 - "format": "nsid" 129 - }, 130 - "rkey": { 131 - "type": "string" 132 - }, 133 - "time_us": { 134 - "type": "integer" 135 - }, 136 - "space": { 137 - "type": "string", 138 - "format": "at-uri", 139 - "description": "Present when the record was read from a permissioned space; its value is the space URI." 140 - }, 141 - "event": { 142 - "type": "ref", 143 - "ref": "#refEventRecord" 144 - } 145 - } 146 - }, 147 - "refEventRecord": { 148 - "type": "object", 149 - "required": [ 150 - "uri", 151 - "did", 152 - "collection", 153 - "rkey", 154 - "time_us" 155 - ], 156 - "properties": { 157 - "uri": { 158 - "type": "string", 159 - "format": "at-uri" 160 - }, 161 - "did": { 162 - "type": "string", 163 - "format": "did" 164 - }, 165 - "collection": { 166 - "type": "string", 167 - "format": "nsid" 168 - }, 169 - "rkey": { 170 - "type": "string" 171 - }, 172 - "cid": { 173 - "type": "string" 174 - }, 175 - "record": { 176 - "type": "ref", 177 - "ref": "community.lexicon.calendar.event#main" 178 - }, 179 - "time_us": { 180 - "type": "integer" 181 - }, 182 - "space": { 183 - "type": "string", 184 - "format": "at-uri", 185 - "description": "Present when the record was read from a permissioned space." 186 - } 187 - } 188 - }, 189 - "profileEntry": { 190 - "type": "object", 191 - "required": [ 192 - "did" 193 - ], 194 - "properties": { 195 - "did": { 196 - "type": "string", 197 - "format": "did" 198 - }, 199 - "handle": { 200 - "type": "string" 201 - }, 202 - "uri": { 203 - "type": "string", 204 - "format": "at-uri" 205 - }, 206 - "cid": { 207 - "type": "string", 208 - "format": "cid" 209 - }, 210 - "value": { 211 - "type": "ref", 212 - "ref": "#appBskyActorProfile" 213 - }, 214 - "collection": { 215 - "type": "string", 216 - "format": "nsid" 217 - }, 218 - "rkey": { 219 - "type": "string" 220 - } 221 - } 222 - }, 223 - "appBskyActorProfile": { 224 - "type": "object", 225 - "properties": { 226 - "avatar": { 227 - "type": "blob", 228 - "accept": [ 229 - "image/png", 230 - "image/jpeg" 231 - ], 232 - "maxSize": 1000000, 233 - "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 234 - }, 235 - "banner": { 236 - "type": "blob", 237 - "accept": [ 238 - "image/png", 239 - "image/jpeg" 240 - ], 241 - "maxSize": 1000000, 242 - "description": "Larger horizontal image to display behind profile view." 243 - }, 244 - "labels": { 245 - "refs": [ 246 - "com.atproto.label.defs#selfLabels" 247 - ], 248 - "type": "union", 249 - "description": "Self-label values, specific to the Bluesky application, on the overall account." 250 - }, 251 - "website": { 252 - "type": "string", 253 - "format": "uri" 254 - }, 255 - "pronouns": { 256 - "type": "string", 257 - "maxLength": 200, 258 - "description": "Free-form pronouns text.", 259 - "maxGraphemes": 20 260 - }, 261 - "createdAt": { 262 - "type": "string", 263 - "format": "datetime" 264 - }, 265 - "pinnedPost": { 266 - "ref": "com.atproto.repo.strongRef", 267 - "type": "ref" 268 - }, 269 - "description": { 270 - "type": "string", 271 - "maxLength": 2560, 272 - "description": "Free-form profile description text.", 273 - "maxGraphemes": 256 274 - }, 275 - "displayName": { 276 - "type": "string", 277 - "maxLength": 640, 278 - "maxGraphemes": 64 279 - }, 280 - "joinedViaStarterPack": { 281 - "ref": "com.atproto.repo.strongRef", 282 - "type": "ref" 283 - } 284 - } 285 - } 286 - } 287 - }
-52
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/addMember.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.addMember", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Add a member to a space. Caller must be the space owner.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "spaceUri", 14 - "did" 15 - ], 16 - "properties": { 17 - "spaceUri": { 18 - "type": "string", 19 - "format": "at-uri" 20 - }, 21 - "did": { 22 - "type": "string", 23 - "format": "did" 24 - } 25 - } 26 - } 27 - }, 28 - "output": { 29 - "encoding": "application/json", 30 - "schema": { 31 - "type": "object", 32 - "required": [ 33 - "ok" 34 - ], 35 - "properties": { 36 - "ok": { 37 - "type": "boolean" 38 - } 39 - } 40 - } 41 - }, 42 - "errors": [ 43 - { 44 - "name": "NotFound" 45 - }, 46 - { 47 - "name": "Forbidden" 48 - } 49 - ] 50 - } 51 - } 52 - }
-55
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/createSpace.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.createSpace", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Create a new space owned by the JWT issuer. The caller is added as an owner-perm member.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "properties": { 13 - "type": { 14 - "type": "string", 15 - "format": "nsid", 16 - "description": "Space type NSID. Defaults to the service's configured type." 17 - }, 18 - "key": { 19 - "type": "string", 20 - "description": "Space key. Auto-generated (TID) if omitted." 21 - }, 22 - "appPolicyRef": { 23 - "type": "string", 24 - "format": "at-uri" 25 - }, 26 - "appPolicy": { 27 - "type": "ref", 28 - "ref": "rsvp.atmo.space.defs#appPolicy" 29 - } 30 - } 31 - } 32 - }, 33 - "output": { 34 - "encoding": "application/json", 35 - "schema": { 36 - "type": "object", 37 - "required": [ 38 - "space" 39 - ], 40 - "properties": { 41 - "space": { 42 - "type": "ref", 43 - "ref": "rsvp.atmo.space.defs#spaceView" 44 - } 45 - } 46 - } 47 - }, 48 - "errors": [ 49 - { 50 - "name": "AlreadyExists" 51 - } 52 - ] 53 - } 54 - } 55 - }
-210
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/defs.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.defs", 4 - "description": "Shared types for permissioned-space XRPC methods.", 5 - "defs": { 6 - "spaceView": { 7 - "type": "object", 8 - "required": [ 9 - "uri", 10 - "ownerDid", 11 - "type", 12 - "key", 13 - "serviceDid", 14 - "createdAt" 15 - ], 16 - "properties": { 17 - "uri": { 18 - "type": "string", 19 - "format": "at-uri" 20 - }, 21 - "ownerDid": { 22 - "type": "string", 23 - "format": "did" 24 - }, 25 - "type": { 26 - "type": "string", 27 - "format": "nsid" 28 - }, 29 - "key": { 30 - "type": "string" 31 - }, 32 - "serviceDid": { 33 - "type": "string" 34 - }, 35 - "appPolicyRef": { 36 - "type": "string", 37 - "format": "at-uri" 38 - }, 39 - "createdAt": { 40 - "type": "integer" 41 - }, 42 - "appPolicy": { 43 - "type": "ref", 44 - "ref": "#appPolicy", 45 - "description": "Owner-only" 46 - } 47 - } 48 - }, 49 - "memberView": { 50 - "type": "object", 51 - "required": [ 52 - "did", 53 - "addedAt" 54 - ], 55 - "properties": { 56 - "did": { 57 - "type": "string", 58 - "format": "did" 59 - }, 60 - "addedAt": { 61 - "type": "integer" 62 - }, 63 - "addedBy": { 64 - "type": "string", 65 - "format": "did" 66 - } 67 - } 68 - }, 69 - "recordView": { 70 - "type": "object", 71 - "required": [ 72 - "spaceUri", 73 - "collection", 74 - "authorDid", 75 - "rkey", 76 - "record", 77 - "createdAt" 78 - ], 79 - "properties": { 80 - "spaceUri": { 81 - "type": "string", 82 - "format": "at-uri" 83 - }, 84 - "collection": { 85 - "type": "string", 86 - "format": "nsid" 87 - }, 88 - "authorDid": { 89 - "type": "string", 90 - "format": "did" 91 - }, 92 - "rkey": { 93 - "type": "string" 94 - }, 95 - "cid": { 96 - "type": "string", 97 - "format": "cid" 98 - }, 99 - "record": { 100 - "type": "unknown" 101 - }, 102 - "createdAt": { 103 - "type": "integer" 104 - } 105 - } 106 - }, 107 - "appPolicy": { 108 - "type": "object", 109 - "required": [ 110 - "mode", 111 - "apps" 112 - ], 113 - "properties": { 114 - "mode": { 115 - "type": "string", 116 - "knownValues": [ 117 - "allow", 118 - "deny" 119 - ], 120 - "description": "'allow' = default-allow with apps[] as denylist; 'deny' = default-deny with apps[] as allowlist." 121 - }, 122 - "apps": { 123 - "type": "array", 124 - "items": { 125 - "type": "string" 126 - } 127 - } 128 - } 129 - }, 130 - "blobInfo": { 131 - "type": "object", 132 - "required": [ 133 - "cid", 134 - "mimeType", 135 - "size", 136 - "authorDid", 137 - "createdAt" 138 - ], 139 - "properties": { 140 - "cid": { 141 - "type": "string", 142 - "format": "cid" 143 - }, 144 - "mimeType": { 145 - "type": "string" 146 - }, 147 - "size": { 148 - "type": "integer" 149 - }, 150 - "authorDid": { 151 - "type": "string", 152 - "format": "did" 153 - }, 154 - "createdAt": { 155 - "type": "integer" 156 - } 157 - } 158 - }, 159 - "inviteView": { 160 - "type": "object", 161 - "required": [ 162 - "tokenHash", 163 - "spaceUri", 164 - "kind", 165 - "usedCount", 166 - "createdBy", 167 - "createdAt" 168 - ], 169 - "properties": { 170 - "tokenHash": { 171 - "type": "string" 172 - }, 173 - "spaceUri": { 174 - "type": "string", 175 - "format": "at-uri" 176 - }, 177 - "kind": { 178 - "type": "string", 179 - "knownValues": [ 180 - "join", 181 - "read", 182 - "read-join" 183 - ] 184 - }, 185 - "expiresAt": { 186 - "type": "integer" 187 - }, 188 - "maxUses": { 189 - "type": "integer" 190 - }, 191 - "usedCount": { 192 - "type": "integer" 193 - }, 194 - "createdBy": { 195 - "type": "string", 196 - "format": "did" 197 - }, 198 - "createdAt": { 199 - "type": "integer" 200 - }, 201 - "revokedAt": { 202 - "type": "integer" 203 - }, 204 - "note": { 205 - "type": "string" 206 - } 207 - } 208 - } 209 - } 210 - }
-56
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/deleteRecord.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.deleteRecord", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Delete a record from a space. Callers can delete their own records; the space owner can delete any record (via a separate admin path).", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "spaceUri", 14 - "collection", 15 - "rkey" 16 - ], 17 - "properties": { 18 - "spaceUri": { 19 - "type": "string", 20 - "format": "at-uri" 21 - }, 22 - "collection": { 23 - "type": "string", 24 - "format": "nsid" 25 - }, 26 - "rkey": { 27 - "type": "string" 28 - } 29 - } 30 - } 31 - }, 32 - "output": { 33 - "encoding": "application/json", 34 - "schema": { 35 - "type": "object", 36 - "required": [ 37 - "ok" 38 - ], 39 - "properties": { 40 - "ok": { 41 - "type": "boolean" 42 - } 43 - } 44 - } 45 - }, 46 - "errors": [ 47 - { 48 - "name": "NotFound" 49 - }, 50 - { 51 - "name": "Forbidden" 52 - } 53 - ] 54 - } 55 - } 56 - }
-42
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/getBlob.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.getBlob", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Read a blob from a space. Requires read access via a service-auth JWT or a read-grant invite token.", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "spaceUri", 12 - "cid" 13 - ], 14 - "properties": { 15 - "spaceUri": { 16 - "type": "string", 17 - "format": "at-uri" 18 - }, 19 - "cid": { 20 - "type": "string", 21 - "format": "cid" 22 - }, 23 - "inviteToken": { 24 - "type": "string", 25 - "description": "Read-grant invite token for anonymous bearer access." 26 - } 27 - } 28 - }, 29 - "output": { 30 - "encoding": "*/*" 31 - }, 32 - "errors": [ 33 - { 34 - "name": "NotFound" 35 - }, 36 - { 37 - "name": "Forbidden" 38 - } 39 - ] 40 - } 41 - } 42 - }
-63
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/getRecord.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.getRecord", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Get a single record from a space.", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "spaceUri", 12 - "collection", 13 - "author", 14 - "rkey" 15 - ], 16 - "properties": { 17 - "spaceUri": { 18 - "type": "string", 19 - "format": "at-uri" 20 - }, 21 - "collection": { 22 - "type": "string", 23 - "format": "nsid" 24 - }, 25 - "author": { 26 - "type": "string", 27 - "format": "did" 28 - }, 29 - "rkey": { 30 - "type": "string" 31 - }, 32 - "inviteToken": { 33 - "type": "string", 34 - "description": "Read-grant invite token. When supplied, replaces JWT auth for this read." 35 - } 36 - } 37 - }, 38 - "output": { 39 - "encoding": "application/json", 40 - "schema": { 41 - "type": "object", 42 - "required": [ 43 - "record" 44 - ], 45 - "properties": { 46 - "record": { 47 - "type": "ref", 48 - "ref": "rsvp.atmo.space.defs#recordView" 49 - } 50 - } 51 - } 52 - }, 53 - "errors": [ 54 - { 55 - "name": "NotFound" 56 - }, 57 - { 58 - "name": "Forbidden" 59 - } 60 - ] 61 - } 62 - } 63 - }
-49
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/getSpace.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.getSpace", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Get metadata for a single space. Caller must be a member, the owner, or hold a read-grant invite token.", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "uri" 12 - ], 13 - "properties": { 14 - "uri": { 15 - "type": "string", 16 - "format": "at-uri" 17 - }, 18 - "inviteToken": { 19 - "type": "string", 20 - "description": "Read-grant invite token. When supplied, replaces JWT auth for this read." 21 - } 22 - } 23 - }, 24 - "output": { 25 - "encoding": "application/json", 26 - "schema": { 27 - "type": "object", 28 - "required": [ 29 - "space" 30 - ], 31 - "properties": { 32 - "space": { 33 - "type": "ref", 34 - "ref": "rsvp.atmo.space.defs#spaceView" 35 - } 36 - } 37 - } 38 - }, 39 - "errors": [ 40 - { 41 - "name": "NotFound" 42 - }, 43 - { 44 - "name": "Forbidden" 45 - } 46 - ] 47 - } 48 - } 49 - }
-48
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/leaveSpace.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.leaveSpace", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Remove the caller from a space's member list. The owner cannot leave — they must delete the space instead.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "spaceUri" 14 - ], 15 - "properties": { 16 - "spaceUri": { 17 - "type": "string", 18 - "format": "at-uri" 19 - } 20 - } 21 - } 22 - }, 23 - "output": { 24 - "encoding": "application/json", 25 - "schema": { 26 - "type": "object", 27 - "required": [ 28 - "ok" 29 - ], 30 - "properties": { 31 - "ok": { 32 - "type": "boolean" 33 - } 34 - } 35 - } 36 - }, 37 - "errors": [ 38 - { 39 - "name": "NotFound" 40 - }, 41 - { 42 - "name": "InvalidRequest", 43 - "description": "Raised if the caller is the space owner." 44 - } 45 - ] 46 - } 47 - } 48 - }
-57
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/listBlobs.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.listBlobs", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "List blob metadata for a space. Members only.", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "spaceUri" 12 - ], 13 - "properties": { 14 - "spaceUri": { 15 - "type": "string", 16 - "format": "at-uri" 17 - }, 18 - "byUser": { 19 - "type": "string", 20 - "format": "did", 21 - "description": "Only blobs uploaded by this DID." 22 - }, 23 - "limit": { 24 - "type": "integer", 25 - "minimum": 1, 26 - "maximum": 200, 27 - "default": 50 28 - }, 29 - "cursor": { 30 - "type": "string" 31 - } 32 - } 33 - }, 34 - "output": { 35 - "encoding": "application/json", 36 - "schema": { 37 - "type": "object", 38 - "required": [ 39 - "blobs" 40 - ], 41 - "properties": { 42 - "blobs": { 43 - "type": "array", 44 - "items": { 45 - "type": "ref", 46 - "ref": "rsvp.atmo.space.defs#blobInfo" 47 - } 48 - }, 49 - "cursor": { 50 - "type": "string" 51 - } 52 - } 53 - } 54 - } 55 - } 56 - } 57 - }
-48
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/listMembers.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.listMembers", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "List members of a space. Caller must be a member or the owner.", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "spaceUri" 12 - ], 13 - "properties": { 14 - "spaceUri": { 15 - "type": "string", 16 - "format": "at-uri" 17 - } 18 - } 19 - }, 20 - "output": { 21 - "encoding": "application/json", 22 - "schema": { 23 - "type": "object", 24 - "required": [ 25 - "members" 26 - ], 27 - "properties": { 28 - "members": { 29 - "type": "array", 30 - "items": { 31 - "type": "ref", 32 - "ref": "rsvp.atmo.space.defs#memberView" 33 - } 34 - } 35 - } 36 - } 37 - }, 38 - "errors": [ 39 - { 40 - "name": "NotFound" 41 - }, 42 - { 43 - "name": "Forbidden" 44 - } 45 - ] 46 - } 47 - } 48 - }
-74
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/listRecords.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.listRecords", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "List records of a given collection within a space. Access is governed by the space's collection policy.", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "spaceUri", 12 - "collection" 13 - ], 14 - "properties": { 15 - "spaceUri": { 16 - "type": "string", 17 - "format": "at-uri" 18 - }, 19 - "collection": { 20 - "type": "string", 21 - "format": "nsid" 22 - }, 23 - "byUser": { 24 - "type": "string", 25 - "format": "did", 26 - "description": "Only return records authored by this DID." 27 - }, 28 - "cursor": { 29 - "type": "string" 30 - }, 31 - "limit": { 32 - "type": "integer", 33 - "minimum": 1, 34 - "maximum": 200, 35 - "default": 50 36 - }, 37 - "inviteToken": { 38 - "type": "string", 39 - "description": "Read-grant invite token. When supplied, replaces JWT auth for this read." 40 - } 41 - } 42 - }, 43 - "output": { 44 - "encoding": "application/json", 45 - "schema": { 46 - "type": "object", 47 - "required": [ 48 - "records" 49 - ], 50 - "properties": { 51 - "records": { 52 - "type": "array", 53 - "items": { 54 - "type": "ref", 55 - "ref": "rsvp.atmo.space.defs#recordView" 56 - } 57 - }, 58 - "cursor": { 59 - "type": "string" 60 - } 61 - } 62 - } 63 - }, 64 - "errors": [ 65 - { 66 - "name": "NotFound" 67 - }, 68 - { 69 - "name": "Forbidden" 70 - } 71 - ] 72 - } 73 - } 74 - }
-62
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/listSpaces.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.listSpaces", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "List spaces the caller has access to. Default scope is 'member' (spaces the caller is a member of, including owned); 'owner' lists only spaces the caller owns. When scope='member', the optional 'owner' param narrows to spaces owned by that DID — useful for listing all channels in a specific community the caller can access.", 8 - "parameters": { 9 - "type": "params", 10 - "properties": { 11 - "scope": { 12 - "type": "string", 13 - "knownValues": [ 14 - "member", 15 - "owner" 16 - ], 17 - "default": "member" 18 - }, 19 - "type": { 20 - "type": "string", 21 - "format": "nsid" 22 - }, 23 - "owner": { 24 - "type": "string", 25 - "format": "did", 26 - "description": "With scope=member, filter to spaces owned by this DID. Ignored when scope=owner." 27 - }, 28 - "cursor": { 29 - "type": "string" 30 - }, 31 - "limit": { 32 - "type": "integer", 33 - "minimum": 1, 34 - "maximum": 200, 35 - "default": 50 36 - } 37 - } 38 - }, 39 - "output": { 40 - "encoding": "application/json", 41 - "schema": { 42 - "type": "object", 43 - "required": [ 44 - "spaces" 45 - ], 46 - "properties": { 47 - "spaces": { 48 - "type": "array", 49 - "items": { 50 - "type": "ref", 51 - "ref": "rsvp.atmo.space.defs#spaceView" 52 - } 53 - }, 54 - "cursor": { 55 - "type": "string" 56 - } 57 - } 58 - } 59 - } 60 - } 61 - } 62 - }
-68
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/putRecord.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.putRecord", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Write a record into a space. The author is always the JWT issuer. If rkey is omitted, a TID is generated.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "spaceUri", 14 - "collection", 15 - "record" 16 - ], 17 - "properties": { 18 - "spaceUri": { 19 - "type": "string", 20 - "format": "at-uri" 21 - }, 22 - "collection": { 23 - "type": "string", 24 - "format": "nsid" 25 - }, 26 - "rkey": { 27 - "type": "string" 28 - }, 29 - "record": { 30 - "type": "unknown" 31 - } 32 - } 33 - } 34 - }, 35 - "output": { 36 - "encoding": "application/json", 37 - "schema": { 38 - "type": "object", 39 - "required": [ 40 - "rkey", 41 - "authorDid", 42 - "createdAt" 43 - ], 44 - "properties": { 45 - "rkey": { 46 - "type": "string" 47 - }, 48 - "authorDid": { 49 - "type": "string", 50 - "format": "did" 51 - }, 52 - "createdAt": { 53 - "type": "integer" 54 - } 55 - } 56 - } 57 - }, 58 - "errors": [ 59 - { 60 - "name": "NotFound" 61 - }, 62 - { 63 - "name": "Forbidden" 64 - } 65 - ] 66 - } 67 - } 68 - }
-55
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/removeMember.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.removeMember", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Remove a member from a space. Owner only. Cannot remove the owner.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "spaceUri", 14 - "did" 15 - ], 16 - "properties": { 17 - "spaceUri": { 18 - "type": "string", 19 - "format": "at-uri" 20 - }, 21 - "did": { 22 - "type": "string", 23 - "format": "did" 24 - } 25 - } 26 - } 27 - }, 28 - "output": { 29 - "encoding": "application/json", 30 - "schema": { 31 - "type": "object", 32 - "required": [ 33 - "ok" 34 - ], 35 - "properties": { 36 - "ok": { 37 - "type": "boolean" 38 - } 39 - } 40 - } 41 - }, 42 - "errors": [ 43 - { 44 - "name": "NotFound" 45 - }, 46 - { 47 - "name": "Forbidden" 48 - }, 49 - { 50 - "name": "InvalidRequest" 51 - } 52 - ] 53 - } 54 - } 55 - }
-50
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/uploadBlob.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.space.uploadBlob", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Upload a blob into a space. Returns a standard atproto BlobRef that records in this space can then reference. The record will be rejected at putRecord time if it references a blob that was not uploaded to the same space.", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "spaceUri" 12 - ], 13 - "properties": { 14 - "spaceUri": { 15 - "type": "string", 16 - "format": "at-uri" 17 - } 18 - } 19 - }, 20 - "input": { 21 - "encoding": "*/*" 22 - }, 23 - "output": { 24 - "encoding": "application/json", 25 - "schema": { 26 - "type": "object", 27 - "required": [ 28 - "blob" 29 - ], 30 - "properties": { 31 - "blob": { 32 - "type": "blob" 33 - } 34 - } 35 - } 36 - }, 37 - "errors": [ 38 - { 39 - "name": "Forbidden" 40 - }, 41 - { 42 - "name": "BlobTooLarge" 43 - }, 44 - { 45 - "name": "InvalidMimeType" 46 - } 47 - ] 48 - } 49 - } 50 - }
-55
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/spaceExt/whoami.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.spaceExt.whoami", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Caller's relationship to a space. For user-owned spaces membership is binary (`isMember`). For community-owned spaces `accessLevel` is also returned, resolved through the community's access-level ladder (including delegated grants).", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "spaceUri" 12 - ], 13 - "properties": { 14 - "spaceUri": { 15 - "type": "string", 16 - "format": "at-uri" 17 - } 18 - } 19 - }, 20 - "output": { 21 - "encoding": "application/json", 22 - "schema": { 23 - "type": "object", 24 - "required": [ 25 - "isOwner", 26 - "isMember" 27 - ], 28 - "properties": { 29 - "isOwner": { 30 - "type": "boolean" 31 - }, 32 - "isMember": { 33 - "type": "boolean" 34 - }, 35 - "accessLevel": { 36 - "type": "string", 37 - "knownValues": [ 38 - "member", 39 - "manager", 40 - "admin", 41 - "owner" 42 - ], 43 - "description": "Only set for community-owned spaces. Null when the caller has no resolvable access." 44 - } 45 - } 46 - } 47 - }, 48 - "errors": [ 49 - { 50 - "name": "NotFound" 51 - } 52 - ] 53 - } 54 - } 55 - }
-67
apps/rsvp-atmo/lexicons/pulled/app/bsky/actor/profile.json
··· 1 - { 2 - "id": "app.bsky.actor.profile", 3 - "defs": { 4 - "main": { 5 - "key": "literal:self", 6 - "type": "record", 7 - "record": { 8 - "type": "object", 9 - "properties": { 10 - "avatar": { 11 - "type": "blob", 12 - "accept": ["image/png", "image/jpeg"], 13 - "maxSize": 1000000, 14 - "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 15 - }, 16 - "banner": { 17 - "type": "blob", 18 - "accept": ["image/png", "image/jpeg"], 19 - "maxSize": 1000000, 20 - "description": "Larger horizontal image to display behind profile view." 21 - }, 22 - "labels": { 23 - "refs": ["com.atproto.label.defs#selfLabels"], 24 - "type": "union", 25 - "description": "Self-label values, specific to the Bluesky application, on the overall account." 26 - }, 27 - "website": { 28 - "type": "string", 29 - "format": "uri" 30 - }, 31 - "pronouns": { 32 - "type": "string", 33 - "maxLength": 200, 34 - "description": "Free-form pronouns text.", 35 - "maxGraphemes": 20 36 - }, 37 - "createdAt": { 38 - "type": "string", 39 - "format": "datetime" 40 - }, 41 - "pinnedPost": { 42 - "ref": "com.atproto.repo.strongRef", 43 - "type": "ref" 44 - }, 45 - "description": { 46 - "type": "string", 47 - "maxLength": 2560, 48 - "description": "Free-form profile description text.", 49 - "maxGraphemes": 256 50 - }, 51 - "displayName": { 52 - "type": "string", 53 - "maxLength": 640, 54 - "maxGraphemes": 64 55 - }, 56 - "joinedViaStarterPack": { 57 - "ref": "com.atproto.repo.strongRef", 58 - "type": "ref" 59 - } 60 - } 61 - }, 62 - "description": "A declaration of a Bluesky account profile." 63 - } 64 - }, 65 - "$type": "com.atproto.lexicon.schema", 66 - "lexicon": 1 67 - }
-142
apps/rsvp-atmo/lexicons/pulled/community/lexicon/calendar/event.json
··· 1 - { 2 - "id": "community.lexicon.calendar.event", 3 - "defs": { 4 - "uri": { 5 - "type": "object", 6 - "required": ["uri"], 7 - "properties": { 8 - "uri": { 9 - "type": "string", 10 - "format": "uri" 11 - }, 12 - "name": { 13 - "type": "string", 14 - "description": "The display name of the URI." 15 - } 16 - }, 17 - "description": "A URI associated with the event." 18 - }, 19 - "main": { 20 - "key": "tid", 21 - "type": "record", 22 - "record": { 23 - "type": "object", 24 - "required": ["createdAt", "name"], 25 - "properties": { 26 - "mode": { 27 - "ref": "community.lexicon.calendar.event#mode", 28 - "type": "ref", 29 - "description": "The attendance mode of the event." 30 - }, 31 - "name": { 32 - "type": "string", 33 - "description": "The name of the event." 34 - }, 35 - "uris": { 36 - "type": "array", 37 - "items": { 38 - "ref": "community.lexicon.calendar.event#uri", 39 - "type": "ref" 40 - }, 41 - "description": "URIs associated with the event." 42 - }, 43 - "endsAt": { 44 - "type": "string", 45 - "format": "datetime", 46 - "description": "Client-declared timestamp when the event ends." 47 - }, 48 - "status": { 49 - "ref": "community.lexicon.calendar.event#status", 50 - "type": "ref", 51 - "description": "The status of the event." 52 - }, 53 - "startsAt": { 54 - "type": "string", 55 - "format": "datetime", 56 - "description": "Client-declared timestamp when the event starts." 57 - }, 58 - "createdAt": { 59 - "type": "string", 60 - "format": "datetime", 61 - "description": "Client-declared timestamp when the event was created." 62 - }, 63 - "locations": { 64 - "type": "array", 65 - "items": { 66 - "refs": [ 67 - "community.lexicon.calendar.event#uri", 68 - "community.lexicon.location.address", 69 - "community.lexicon.location.fsq", 70 - "community.lexicon.location.geo", 71 - "community.lexicon.location.hthree" 72 - ], 73 - "type": "union" 74 - }, 75 - "description": "The locations where the event takes place." 76 - }, 77 - "description": { 78 - "type": "string", 79 - "description": "The description of the event." 80 - } 81 - } 82 - }, 83 - "description": "A calendar event." 84 - }, 85 - "mode": { 86 - "type": "string", 87 - "default": "community.lexicon.calendar.event#inperson", 88 - "description": "The mode of the event.", 89 - "knownValues": [ 90 - "community.lexicon.calendar.event#hybrid", 91 - "community.lexicon.calendar.event#inperson", 92 - "community.lexicon.calendar.event#virtual" 93 - ] 94 - }, 95 - "hybrid": { 96 - "type": "token", 97 - "description": "A hybrid event that takes place both online and offline." 98 - }, 99 - "status": { 100 - "type": "string", 101 - "default": "community.lexicon.calendar.event#scheduled", 102 - "description": "The status of the event.", 103 - "knownValues": [ 104 - "community.lexicon.calendar.event#cancelled", 105 - "community.lexicon.calendar.event#planned", 106 - "community.lexicon.calendar.event#postponed", 107 - "community.lexicon.calendar.event#rescheduled", 108 - "community.lexicon.calendar.event#scheduled" 109 - ] 110 - }, 111 - "planned": { 112 - "type": "token", 113 - "description": "The event has been created, but not finalized." 114 - }, 115 - "virtual": { 116 - "type": "token", 117 - "description": "A virtual event that takes place online." 118 - }, 119 - "inperson": { 120 - "type": "token", 121 - "description": "An in-person event that takes place offline." 122 - }, 123 - "cancelled": { 124 - "type": "token", 125 - "description": "The event has been cancelled." 126 - }, 127 - "postponed": { 128 - "type": "token", 129 - "description": "The event has been postponed and a new start date has not been set." 130 - }, 131 - "scheduled": { 132 - "type": "token", 133 - "description": "The event has been created and scheduled." 134 - }, 135 - "rescheduled": { 136 - "type": "token", 137 - "description": "The event has been rescheduled." 138 - } 139 - }, 140 - "$type": "com.atproto.lexicon.schema", 141 - "lexicon": 1 142 - }
-43
apps/rsvp-atmo/lexicons/pulled/community/lexicon/calendar/rsvp.json
··· 1 - { 2 - "id": "community.lexicon.calendar.rsvp", 3 - "defs": { 4 - "main": { 5 - "key": "tid", 6 - "type": "record", 7 - "record": { 8 - "type": "object", 9 - "required": ["subject", "status"], 10 - "properties": { 11 - "status": { 12 - "type": "string", 13 - "default": "community.lexicon.calendar.rsvp#going", 14 - "knownValues": [ 15 - "community.lexicon.calendar.rsvp#interested", 16 - "community.lexicon.calendar.rsvp#going", 17 - "community.lexicon.calendar.rsvp#notgoing" 18 - ] 19 - }, 20 - "subject": { 21 - "ref": "com.atproto.repo.strongRef", 22 - "type": "ref" 23 - } 24 - } 25 - }, 26 - "description": "An RSVP for an event." 27 - }, 28 - "going": { 29 - "type": "token", 30 - "description": "Going to the event" 31 - }, 32 - "notgoing": { 33 - "type": "token", 34 - "description": "Not going to the event" 35 - }, 36 - "interested": { 37 - "type": "token", 38 - "description": "Interested in the event" 39 - } 40 - }, 41 - "$type": "com.atproto.lexicon.schema", 42 - "lexicon": 1 43 - }
-40
apps/rsvp-atmo/lexicons/pulled/community/lexicon/location/address.json
··· 1 - { 2 - "id": "community.lexicon.location.address", 3 - "defs": { 4 - "main": { 5 - "type": "object", 6 - "required": ["country"], 7 - "properties": { 8 - "name": { 9 - "type": "string", 10 - "description": "The name of the location." 11 - }, 12 - "region": { 13 - "type": "string", 14 - "description": "The administrative region of the country. For example, a state in the USA." 15 - }, 16 - "street": { 17 - "type": "string", 18 - "description": "The street address." 19 - }, 20 - "country": { 21 - "type": "string", 22 - "maxLength": 10, 23 - "minLength": 2, 24 - "description": "The ISO 3166 country code. Preferably the 2-letter code." 25 - }, 26 - "locality": { 27 - "type": "string", 28 - "description": "The locality of the region. For example, a city in the USA." 29 - }, 30 - "postalCode": { 31 - "type": "string", 32 - "description": "The postal code of the location." 33 - } 34 - }, 35 - "description": "A physical location in the form of a street address." 36 - } 37 - }, 38 - "$type": "com.atproto.lexicon.schema", 39 - "lexicon": 1 40 - }
-28
apps/rsvp-atmo/lexicons/pulled/community/lexicon/location/fsq.json
··· 1 - { 2 - "id": "community.lexicon.location.fsq", 3 - "defs": { 4 - "main": { 5 - "type": "object", 6 - "required": ["fsq_place_id"], 7 - "properties": { 8 - "name": { 9 - "type": "string", 10 - "description": "The name of the location." 11 - }, 12 - "latitude": { 13 - "type": "string" 14 - }, 15 - "longitude": { 16 - "type": "string" 17 - }, 18 - "fsq_place_id": { 19 - "type": "string", 20 - "description": "The unique identifier of a Foursquare POI." 21 - } 22 - }, 23 - "description": "A physical location contained in the Foursquare Open Source Places dataset." 24 - } 25 - }, 26 - "$type": "com.atproto.lexicon.schema", 27 - "lexicon": 1 28 - }
-27
apps/rsvp-atmo/lexicons/pulled/community/lexicon/location/geo.json
··· 1 - { 2 - "id": "community.lexicon.location.geo", 3 - "defs": { 4 - "main": { 5 - "type": "object", 6 - "required": ["latitude", "longitude"], 7 - "properties": { 8 - "name": { 9 - "type": "string", 10 - "description": "The name of the location." 11 - }, 12 - "altitude": { 13 - "type": "string" 14 - }, 15 - "latitude": { 16 - "type": "string" 17 - }, 18 - "longitude": { 19 - "type": "string" 20 - } 21 - }, 22 - "description": "A physical location in the form of a WGS84 coordinate." 23 - } 24 - }, 25 - "$type": "com.atproto.lexicon.schema", 26 - "lexicon": 1 27 - }
-22
apps/rsvp-atmo/lexicons/pulled/community/lexicon/location/hthree.json
··· 1 - { 2 - "id": "community.lexicon.location.hthree", 3 - "defs": { 4 - "main": { 5 - "type": "object", 6 - "required": ["value"], 7 - "properties": { 8 - "name": { 9 - "type": "string", 10 - "description": "The name of the location." 11 - }, 12 - "value": { 13 - "type": "string", 14 - "description": "The h3 encoded location." 15 - } 16 - }, 17 - "description": "A physical location in the form of a H3 encoded location." 18 - } 19 - }, 20 - "$type": "com.atproto.lexicon.schema", 21 - "lexicon": 1 22 - }
-58
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/create.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.space.create", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Create a community-owned space (group/role/channel). Caller needs `admin` or higher in the community's `$admin` space. Reserved keys (`$admin`, `$publishers`, …) are rejected here.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "communityDid" 14 - ], 15 - "properties": { 16 - "communityDid": { 17 - "type": "string", 18 - "format": "did" 19 - }, 20 - "key": { 21 - "type": "string", 22 - "description": "Space key. Auto-generated if omitted." 23 - } 24 - } 25 - } 26 - }, 27 - "output": { 28 - "encoding": "application/json", 29 - "schema": { 30 - "type": "object", 31 - "required": [ 32 - "space" 33 - ], 34 - "properties": { 35 - "space": { 36 - "type": "ref", 37 - "ref": "rsvp.atmo.community.defs#spaceView" 38 - } 39 - } 40 - } 41 - }, 42 - "errors": [ 43 - { 44 - "name": "InvalidRequest" 45 - }, 46 - { 47 - "name": "NotFound" 48 - }, 49 - { 50 - "name": "Forbidden" 51 - }, 52 - { 53 - "name": "AlreadyExists" 54 - } 55 - ] 56 - } 57 - } 58 - }
-47
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/delete.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.space.delete", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Soft-delete a community-owned space. Caller must be `owner` on the space, or `admin`+ in the community's `$admin` space. Reserved spaces cannot be deleted.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "spaceUri" 14 - ], 15 - "properties": { 16 - "spaceUri": { 17 - "type": "string", 18 - "format": "at-uri" 19 - } 20 - } 21 - } 22 - }, 23 - "output": { 24 - "encoding": "application/json", 25 - "schema": { 26 - "type": "object", 27 - "required": [ 28 - "ok" 29 - ], 30 - "properties": { 31 - "ok": { 32 - "type": "boolean" 33 - } 34 - } 35 - } 36 - }, 37 - "errors": [ 38 - { 39 - "name": "NotFound" 40 - }, 41 - { 42 - "name": "Forbidden" 43 - } 44 - ] 45 - } 46 - } 47 - }
-59
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/deleteRecord.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.space.deleteRecord", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Delete an in-space record authored by the community DID. Caller must be `admin` or higher in the target space.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "spaceUri", 14 - "collection", 15 - "rkey" 16 - ], 17 - "properties": { 18 - "spaceUri": { 19 - "type": "string", 20 - "format": "at-uri" 21 - }, 22 - "collection": { 23 - "type": "string", 24 - "format": "nsid" 25 - }, 26 - "rkey": { 27 - "type": "string" 28 - } 29 - } 30 - } 31 - }, 32 - "output": { 33 - "encoding": "application/json", 34 - "schema": { 35 - "type": "object", 36 - "required": [ 37 - "ok" 38 - ], 39 - "properties": { 40 - "ok": { 41 - "type": "boolean" 42 - } 43 - } 44 - } 45 - }, 46 - "errors": [ 47 - { 48 - "name": "InvalidRequest" 49 - }, 50 - { 51 - "name": "NotFound" 52 - }, 53 - { 54 - "name": "Forbidden" 55 - } 56 - ] 57 - } 58 - } 59 - }
-60
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/grant.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.space.grant", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Grant or upsert a subject's access level on a space. Caller needs `manager` or higher; the granted level cannot exceed the caller's own. A subject can be a DID (member) or another space (delegated membership).", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "spaceUri", 14 - "subject", 15 - "accessLevel" 16 - ], 17 - "properties": { 18 - "spaceUri": { 19 - "type": "string", 20 - "format": "at-uri" 21 - }, 22 - "subject": { 23 - "type": "ref", 24 - "ref": "rsvp.atmo.community.defs#subject" 25 - }, 26 - "accessLevel": { 27 - "type": "ref", 28 - "ref": "rsvp.atmo.community.defs#accessLevel" 29 - } 30 - } 31 - } 32 - }, 33 - "output": { 34 - "encoding": "application/json", 35 - "schema": { 36 - "type": "object", 37 - "required": [ 38 - "ok" 39 - ], 40 - "properties": { 41 - "ok": { 42 - "type": "boolean" 43 - } 44 - } 45 - } 46 - }, 47 - "errors": [ 48 - { 49 - "name": "InvalidRequest" 50 - }, 51 - { 52 - "name": "NotFound" 53 - }, 54 - { 55 - "name": "Forbidden" 56 - } 57 - ] 58 - } 59 - } 60 - }
-58
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/listMembers.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.space.listMembers", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "List members of a community-owned space. By default returns the raw access-level rows; with `flatten=true` returns the flattened DID list (delegated memberships resolved).", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "spaceUri" 12 - ], 13 - "properties": { 14 - "spaceUri": { 15 - "type": "string", 16 - "format": "at-uri" 17 - }, 18 - "flatten": { 19 - "type": "boolean", 20 - "description": "If true, return the flat DID list instead of raw rows." 21 - } 22 - } 23 - }, 24 - "output": { 25 - "encoding": "application/json", 26 - "schema": { 27 - "type": "object", 28 - "properties": { 29 - "rows": { 30 - "type": "array", 31 - "items": { 32 - "type": "ref", 33 - "ref": "rsvp.atmo.community.defs#memberRow" 34 - }, 35 - "description": "Present when flatten=false." 36 - }, 37 - "members": { 38 - "type": "array", 39 - "items": { 40 - "type": "ref", 41 - "ref": "rsvp.atmo.community.defs#flatMember" 42 - }, 43 - "description": "Present when flatten=true." 44 - } 45 - } 46 - } 47 - }, 48 - "errors": [ 49 - { 50 - "name": "NotFound" 51 - }, 52 - { 53 - "name": "Forbidden" 54 - } 55 - ] 56 - } 57 - } 58 - }
-71
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/putRecord.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.space.putRecord", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Write an in-space record authored by the community DID (rather than the caller). Caller must be `admin` or higher in the target space.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "spaceUri", 14 - "collection", 15 - "record" 16 - ], 17 - "properties": { 18 - "spaceUri": { 19 - "type": "string", 20 - "format": "at-uri" 21 - }, 22 - "collection": { 23 - "type": "string", 24 - "format": "nsid" 25 - }, 26 - "rkey": { 27 - "type": "string" 28 - }, 29 - "record": { 30 - "type": "unknown" 31 - } 32 - } 33 - } 34 - }, 35 - "output": { 36 - "encoding": "application/json", 37 - "schema": { 38 - "type": "object", 39 - "required": [ 40 - "rkey", 41 - "authorDid", 42 - "createdAt" 43 - ], 44 - "properties": { 45 - "rkey": { 46 - "type": "string" 47 - }, 48 - "authorDid": { 49 - "type": "string", 50 - "format": "did" 51 - }, 52 - "createdAt": { 53 - "type": "integer" 54 - } 55 - } 56 - } 57 - }, 58 - "errors": [ 59 - { 60 - "name": "InvalidRequest" 61 - }, 62 - { 63 - "name": "NotFound" 64 - }, 65 - { 66 - "name": "Forbidden" 67 - } 68 - ] 69 - } 70 - } 71 - }
-47
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/resync.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.space.resync", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Manually recompute `spaces_members` for a community-owned space. Useful after a crash mid-reconcile leaves stale membership. Caller must have `admin` or higher.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "spaceUri" 14 - ], 15 - "properties": { 16 - "spaceUri": { 17 - "type": "string", 18 - "format": "at-uri" 19 - } 20 - } 21 - } 22 - }, 23 - "output": { 24 - "encoding": "application/json", 25 - "schema": { 26 - "type": "object", 27 - "required": [ 28 - "ok" 29 - ], 30 - "properties": { 31 - "ok": { 32 - "type": "boolean" 33 - } 34 - } 35 - } 36 - }, 37 - "errors": [ 38 - { 39 - "name": "NotFound" 40 - }, 41 - { 42 - "name": "Forbidden" 43 - } 44 - ] 45 - } 46 - } 47 - }
-55
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/revoke.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.space.revoke", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Remove a subject's access grant from a space. Caller must have `manager` or higher and outrank the subject's current level.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "spaceUri", 14 - "subject" 15 - ], 16 - "properties": { 17 - "spaceUri": { 18 - "type": "string", 19 - "format": "at-uri" 20 - }, 21 - "subject": { 22 - "type": "ref", 23 - "ref": "rsvp.atmo.community.defs#subject" 24 - } 25 - } 26 - } 27 - }, 28 - "output": { 29 - "encoding": "application/json", 30 - "schema": { 31 - "type": "object", 32 - "required": [ 33 - "ok" 34 - ], 35 - "properties": { 36 - "ok": { 37 - "type": "boolean" 38 - } 39 - } 40 - } 41 - }, 42 - "errors": [ 43 - { 44 - "name": "InvalidRequest" 45 - }, 46 - { 47 - "name": "NotFound" 48 - }, 49 - { 50 - "name": "Forbidden" 51 - } 52 - ] 53 - } 54 - } 55 - }
-60
apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/setAccessLevel.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "rsvp.atmo.community.space.setAccessLevel", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Change the access level of an existing grant. Caller must outrank both the old and new level.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "spaceUri", 14 - "subject", 15 - "accessLevel" 16 - ], 17 - "properties": { 18 - "spaceUri": { 19 - "type": "string", 20 - "format": "at-uri" 21 - }, 22 - "subject": { 23 - "type": "ref", 24 - "ref": "rsvp.atmo.community.defs#subject" 25 - }, 26 - "accessLevel": { 27 - "type": "ref", 28 - "ref": "rsvp.atmo.community.defs#accessLevel" 29 - } 30 - } 31 - } 32 - }, 33 - "output": { 34 - "encoding": "application/json", 35 - "schema": { 36 - "type": "object", 37 - "required": [ 38 - "ok" 39 - ], 40 - "properties": { 41 - "ok": { 42 - "type": "boolean" 43 - } 44 - } 45 - } 46 - }, 47 - "errors": [ 48 - { 49 - "name": "InvalidRequest" 50 - }, 51 - { 52 - "name": "NotFound" 53 - }, 54 - { 55 - "name": "Forbidden" 56 - } 57 - ] 58 - } 59 - } 60 - }