Compare commits
383 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0770688814 | |||
| 5af5deff55 | |||
| 087515e6a4 | |||
| 877e6293b9 | |||
| 5d3e248c26 | |||
| 6467208918 | |||
| 17c394d9b1 | |||
| 03445eef45 | |||
| 98d1d019c1 | |||
| 3cb156907a | |||
| 84eb5ed95d | |||
| dacb0e8005 | |||
| 750cfe9020 | |||
| e1d5e63314 | |||
| c531144d24 | |||
| f93115f2e7 | |||
| 1d575edac9 | |||
| ff9f84e11a | |||
| 0cfb16b2bf | |||
| 26d793af95 | |||
| 55d06de80a | |||
| 61e75af39e | |||
| 952354c1dd | |||
| b3ce11ae2e | |||
| b704c52327 | |||
| 7c61f196c3 | |||
| 2a9221d244 | |||
| 55c39860c8 | |||
| 83b7246439 | |||
| d8801e2a17 | |||
| aa1763df0f | |||
| b6b231f9c0 | |||
| f6f100c337 | |||
| 7000746941 | |||
| c62651dd69 | |||
| 284e6fbca9 | |||
| 9def38a5f6 | |||
| 158daf65fc | |||
| 42232c7f36 | |||
| 3b9af1bdb6 | |||
| b6a9625e36 | |||
| 3711454321 | |||
| 2a5e2c267f | |||
| 34f9ae1f68 | |||
| 2203d825e8 | |||
| 4f11fc341a | |||
| c224e1fae6 | |||
| 6116d56d36 | |||
| ac6bd18f1d | |||
| c5c6c7dac3 | |||
| fac213dfdc | |||
| 1479c87c6b | |||
| 960a02b8d5 | |||
| 36e3019711 | |||
| a99dd58b5f | |||
| 0dbea15da4 | |||
| 3aa18f84ef | |||
| ae22362c59 | |||
| 37b2f492bb | |||
| d0c136cba2 | |||
| c4df84cd78 | |||
| e5e5bcf85f | |||
| 527f5d4c5d | |||
| 57ca2109f3 | |||
| b86dbfcbe3 | |||
| db795be786 | |||
| 8445e8a9aa | |||
| 737c046e95 | |||
| df8328c3c2 | |||
| b2baf8d660 | |||
| 6a88461c5a | |||
| 3b41b81e27 | |||
| 4ecde4879f | |||
| 13b2aad9b6 | |||
| efb2b58fb2 | |||
| cbbc03b6e4 | |||
| 3d60e1af7c | |||
| c89225f972 | |||
| bc69b03f53 | |||
| ff42a3afc3 | |||
| 266b5631d2 | |||
| 139b2941d6 | |||
| 67eb0c19ac | |||
| 112b159ec2 | |||
| eaee3d4305 | |||
| 9f9f71a6a3 | |||
| 5f73f8ed72 | |||
| 192685fca7 | |||
| 3dc3791543 | |||
| c9004b4d98 | |||
| 7ed57043c1 | |||
| b3f80b457c | |||
| 7444295e79 | |||
| 5a2f8847ef | |||
| 383e157021 | |||
| 39ea120dae | |||
| 76ab6eaa7b | |||
| 82e708095e | |||
| 0e5df95e3a | |||
| 66341400d4 | |||
| 88326d2833 | |||
| f7c26103a6 | |||
| 1c473ab555 | |||
| d5f4224f73 | |||
| 138f0a0b37 | |||
| e8969b6b3d | |||
| 137065b333 | |||
| 52834aba18 | |||
| 1b83d841cf | |||
| d69c81e335 | |||
| cb5f9bfc0c | |||
| 450f085663 | |||
| 38852f0ddf | |||
| c4b04899c8 | |||
| 1521b2ebba | |||
| 8603be629a | |||
| 7229c03a2a | |||
| dc9644bbeb | |||
| 66b72950e8 | |||
| caad88223f | |||
| ce87b88e33 | |||
| 7cf7d9062d | |||
| a23d13268c | |||
| ea5df64db9 | |||
| 39dea03c41 | |||
| cd9deb6f21 | |||
| 0e8d4bb62b | |||
| 009c4afd36 | |||
| 39cf199df5 | |||
| 2335075a20 | |||
| 978a99efad | |||
| 7e3eba638f | |||
| 48aee6c92c | |||
| ae220d593b | |||
| 6ae3cf6131 | |||
| 619068fb81 | |||
| 95e377504a | |||
| 379cae6986 | |||
| 60f84ebe47 | |||
| 50dfbec512 | |||
| f8c84ebd3d | |||
| b619ae87a8 | |||
| 8711f6e1b8 | |||
| 23a13d7dde | |||
| fdbbff8543 | |||
| 27cdeb4e86 | |||
| a2e4e95847 | |||
| ed61c62f31 | |||
| 9fdfb8a3ad | |||
| 78fee144ff | |||
| 00e6dbdcea | |||
| 19ecf5f76a | |||
| 7b9a98a763 | |||
| c1a10e1992 | |||
| 4f5c400127 | |||
| 915f9065d3 | |||
| 25cea46aaa | |||
| 369bb02b9f | |||
| 3ba21f9a36 | |||
| 49b38069ed | |||
| 8a725a9d69 | |||
| 143a5f2251 | |||
| 2830daeaa6 | |||
| d54d7077eb | |||
| 78e8ec25df | |||
| 595dd20323 | |||
| e6e43f3ad3 | |||
| 5905eb94a6 | |||
| ce356cde55 | |||
| 2c4eb7fe20 | |||
| ece2ee8533 | |||
| ecf59214d5 | |||
| 99963d002a | |||
| 9bdd35881a | |||
| aa22728888 | |||
| cbd5837f01 | |||
| aaedafc6ba | |||
| b3d1819f49 | |||
| 7310b9704d | |||
| fa4ef5e815 | |||
| e9dc15bd79 | |||
| 538d39e19d | |||
| 3fbec803ed | |||
| 6c7160ff7b | |||
| d8e53636c8 | |||
| f51289f35b | |||
| 162aa5f82a | |||
| 9f06e9a31d | |||
| ccd8964331 | |||
| f7ae44d7f6 | |||
| a6cf4b29b6 | |||
| f58029ba54 | |||
| 716819fb25 | |||
| c2615a7eee | |||
| 3ffebd0bf2 | |||
| 0193467463 | |||
| dfd4bee4ab | |||
| 08db98a744 | |||
| 6752056f46 | |||
| 5a384f136a | |||
| d2ed7311a2 | |||
| 0b03086d35 | |||
| d78484f94d | |||
| 5b19d390b3 | |||
| d40a66dfe1 | |||
| b329266853 | |||
| 1a87be24d6 | |||
| 7750b91500 | |||
| e3baf89412 | |||
| 7ef893563b | |||
| d5414b0b14 | |||
| 8ffbaccc68 | |||
| 63f97ae9c1 | |||
| 0093727fc4 | |||
| 2f78893ace | |||
| 141f91b2fa | |||
| e3357e9c78 | |||
| e60af07248 | |||
| e9f065fce8 | |||
| 43633250f4 | |||
| 9a11a674ab | |||
| cbd7c677bc | |||
| e1ca2ff2a4 | |||
| 627f49e33e | |||
| 87e335af62 | |||
| cffd7e98bc | |||
| 538e3b0d55 | |||
| 8e759970af | |||
| 9af7a2d122 | |||
| 95bd491ecb | |||
| ce497e8543 | |||
| c4b967ff70 | |||
| 10cf1c1018 | |||
| 3cf5b31a2d | |||
| e15e7cf1a0 | |||
| a164f89535 | |||
| 5bff33f7ce | |||
| 57811e4647 | |||
| c7f5a86883 | |||
| e6b6107e02 | |||
| a78380c89c | |||
| 95ef3c24e8 | |||
| 5d8e7a6588 | |||
| 64dc2ed0a6 | |||
| dd0fe8d5e7 | |||
| 790fd3cb50 | |||
| 046efb3f71 | |||
| f9ddcb253f | |||
| d56fb0d1d4 | |||
| 4c6d37eace | |||
| 2e5a24cb87 | |||
| b4a76bae7b | |||
| 5605e4d619 | |||
| 9917b9f1c8 | |||
| 13edecd0d3 | |||
| d818622dc2 | |||
| 5622bcf023 | |||
| 73c41b669d | |||
| 0c26509f26 | |||
| 1168691c36 | |||
| 45928bfcd6 | |||
| c05dd09159 | |||
| 1a05fbb7a6 | |||
| d698904c4c | |||
| 397c62fe9c | |||
| e266405a89 | |||
| fa265521f0 | |||
| 39fa57080f | |||
| 6fab3940f6 | |||
| f4cef84494 | |||
| 104e6ed37b | |||
| 7eb99f2ac7 | |||
| 380aa3bfe7 | |||
| a6e16cd0cd | |||
| fb95dc6e6f | |||
| d551b0e352 | |||
| 693784c3a0 | |||
| b570086f08 | |||
| cfa01edb9c | |||
| 308f432f7a | |||
| e39ca114ac | |||
| 7e1d45877b | |||
| b0ecba0211 | |||
| dc08244c24 | |||
| 90f15698ee | |||
| 2d13d05ab0 | |||
| b643521a5e | |||
| 6b28b116f8 | |||
| 468980f228 | |||
| 86ff431100 | |||
| a487ab8afe | |||
| 03a2a4bb38 | |||
| 8012627a12 | |||
| a17d2e8a59 | |||
| 688f9d6759 | |||
| 6e55972a11 | |||
| bcae2b77b6 | |||
| 1d445af2a7 | |||
| 9f87ff3aa6 | |||
| 0adc852c31 | |||
| d396de016d | |||
| 0f2fc6592f | |||
| be8e6c1f25 | |||
| 86f6860418 | |||
| 23c271fcac | |||
| d4d23e1950 | |||
| 5ffd612990 | |||
| 151a7b0c6d | |||
| 7456f53e45 | |||
| 72d4bfcfc5 | |||
| 46550d6c1f | |||
| 69f53d632d | |||
| 4dede90ea5 | |||
| aeb9b69306 | |||
| 3182731f11 | |||
| 261968fd65 | |||
| 39ab1b06bd | |||
| 0748d919d1 | |||
| 1d1d94e80d | |||
| 351f024e7a | |||
| 4021b08a58 | |||
| 032abf6b50 | |||
| 1dd20d3a81 | |||
| 9115325c7b | |||
| b7bc95d6ab | |||
| 3628cebfbb | |||
| f8e56b387f | |||
| 82e75bf6c1 | |||
| 532b22822f | |||
| 92702322c3 | |||
| 6f59d64aa1 | |||
| 39b5c08e07 | |||
| 94ddc8219c | |||
| 10021207b6 | |||
| 756ab5d87f | |||
| 3202dce91b | |||
| fc9f8e3ea0 | |||
| 15fe80c39f | |||
| 632e1ba91f | |||
| 7c1429fe36 | |||
| cdb6b27fc6 | |||
| d764c54fe4 | |||
| 1d219ff3f6 | |||
| 014674dce7 | |||
| 601401afce | |||
| a37a7de809 | |||
| f496fa2c4d | |||
| 33435c2e83 | |||
| 8ff5ec4caa | |||
| 1e702ea675 | |||
| 1a7bd8c0d2 | |||
| 8c03d75b6c | |||
| a8e307ef48 | |||
| ac1d29ec0f | |||
| dcf7ccd2fe | |||
| c518cf4f62 | |||
| 1e49025331 | |||
| dca500186e | |||
| 3cd613f2f6 | |||
| ca768fe1b0 | |||
| 386f583cff | |||
| 4c15fb47ce | |||
| 1cb5b9b80c | |||
| bd469a6861 | |||
| 9c6b711c2e | |||
| e71d265dd4 | |||
| 591de19524 | |||
| 09439d7d68 | |||
| f560cf2266 | |||
| fefe70126d | |||
| 93516238c9 | |||
| d239f0bcf5 | |||
| d7154a7889 | |||
| dba2d30e2c | |||
| 7f38ee03c0 | |||
| 778f76320e | |||
| 8dd77bad2f | |||
| 1a5d7427bf | |||
| c16080e2c6 | |||
| 7d0ecd8f3e | |||
| 8dc4caf558 | |||
| 0c20539c71 | |||
| 86bb25768d |
@@ -0,0 +1,904 @@
|
||||
---
|
||||
name: backend-lexification
|
||||
description: >
|
||||
Migrate backend apis from `@atproto/api`, `@atproto/lexicon`, `@atproto/xrpc`,
|
||||
and `@atproto/lex-cli` to `@atproto/lex`. Use this skill whenever the user
|
||||
wants to adopt `@atproto/lex` in a package that currently relies on
|
||||
`@atproto/lex-cli` to perform code generation, replace AtpAgent with Client,
|
||||
migrate generated lexicon code to the new `lex build` output, replace
|
||||
`ids.XxxYyy` with namespace accessors, adopt branded string types, switch from
|
||||
`jsonStringToLex` to `lexParse`, or any refactoring that moves code from the
|
||||
old generated lexicon system to `@atproto/lex`. Also trigger when the user
|
||||
mentions "lexification", "lex SDK", "lex migration", or asks about replacing
|
||||
`@atproto/api` imports in service code.
|
||||
---
|
||||
|
||||
# Lexification: Migrating to `@atproto/lex`
|
||||
|
||||
This skill describes how to refactor an AT Protocol service package to replace:
|
||||
|
||||
- `@atproto/api` (the old high-level client)
|
||||
- `@atproto/lexicon` (the old runtime lexicon library)
|
||||
- `@atproto/xrpc` (the old XRPC client types)
|
||||
- `@atproto/lex-cli` codegen (the old code generator that produced `src/lexicon/` directories)
|
||||
|
||||
...with `@atproto/lex`, which provides generated TypeScript schemas with type-safe validation, type guards, builders, and an XRPC client.
|
||||
|
||||
For the full `@atproto/lex` API reference, read `packages/lex/lex/README.md`.
|
||||
|
||||
## Guiding Principles
|
||||
|
||||
- **Minimize runtime changes.** Prefer type-level changes over runtime changes. If the old code works correctly at runtime, keep its logic and just improve the types around it.
|
||||
- **Tests still use `@atproto/api` exclusively.** Do not migrate test files — they will be migrated in a separate phase. Tests that imported from the old `src/lexicon/` should switch to importing from `@atproto/api` instead.
|
||||
|
||||
## What `@atproto/lex` Provides
|
||||
|
||||
- **Generated TypeScript schemas** via `lex build` (replaces `lex-cli`'s `lex gen-server`)
|
||||
- **Runtime utilities**: `lexParse`, `lexStringify`, `toDatetimeString`, `currentDatetimeString`
|
||||
- **Branded string types**: `DidString`, `HandleString`, `AtIdentifierString`, `AtUriString`, `UriString`, `DatetimeString`
|
||||
- **Type guards**: `isDidString`, `isHandleString`, `isAtIdentifierString`, `isAtUriString`
|
||||
- **Data types**: `Cid`, `parseCid`, `BlobRef` (from `@atproto/lex-data` sub-export, also re-exported from `@atproto/lex`)
|
||||
- **`Client` class**: replaces `AtpAgent` for service-to-service XRPC calls
|
||||
- **`xrpc` / `xrpcSafe` functions**: standalone XRPC requests with structured error handling
|
||||
- **`XrpcError`**: replaces `XRPCError` from `@atproto/xrpc`
|
||||
- **Type-safe schema accessors**: `$type`, `$matches`, `$isTypeOf`, `$build`, `$safeParse`, `$lxm`, etc.
|
||||
|
||||
The generated schemas live in `src/lexicons/` (note the plural). They expose a namespace-based API (e.g., `app.bsky.feed.post`) with `$type`, `$build`, `$check`, `$matches`, `$isTypeOf`, `$parse`, `$validate`, `$safeParse`, `$lxm`, and other utilities.
|
||||
|
||||
## Overview of Changes
|
||||
|
||||
The refactor touches these areas (in recommended order):
|
||||
|
||||
1. **Project Configuration** - Update `package.json` dependencies, build scripts and git configuration
|
||||
2. **Project Code Setup** - Replace server creation, endpoint registration, imports from old generated code, app context initialization (`AtpAgent` → `Client`), and type aliases with new patterns from `@atproto/lex`
|
||||
3. **Endpoint Handlers Registration** - New `server.add()` pattern, handler return types, LXM references, and parameter/output/record type replacements
|
||||
4. **XRPC Client Calls** - Replace `AtpAgent` API calls with `Client.xrpc()`, `Client.call()`, or `xrpcSafe()`
|
||||
5. **Type Strictness** - Use branded types (`DidString`, `HandleString`, `AtUriString`, `Cid`, etc.)
|
||||
6. **Data Utilities** - Replace old data manipulation functions (`jsonStringToLex` → `lexParse`, datetime helpers, `BlobRef`, etc.)
|
||||
7. **Schema Validation and Type Guards** - Replace old `isX()` functions with `$matches()` / `$isTypeOf` / `$build()`
|
||||
|
||||
## 1. Project Configuration
|
||||
|
||||
### Dependencies
|
||||
|
||||
Add `@atproto/lex` as a dependency.
|
||||
|
||||
Remove from dependencies (if present):
|
||||
|
||||
- `multiformats` (CID handling is now in `@atproto/lex-data`)
|
||||
|
||||
Remove from devDependencies:
|
||||
|
||||
- `@atproto/api` (unless still needed for tests in this phase)
|
||||
- `@atproto/lexicon`
|
||||
- `@atproto/lex-cli`
|
||||
- `@atproto/xrpc`
|
||||
|
||||
### Lexicon installation
|
||||
|
||||
Remove the old `src/lexicon/` directory (singular) and any codegen scripts that referenced `lex gen-server`. We will also remove the (manually maintained) `./lexicons/` directory in order to manage installed lexicons with the new `lex install` commands:
|
||||
|
||||
```sh
|
||||
rm -rf ./src/lexicon
|
||||
rm -rf ./lexicons
|
||||
```
|
||||
|
||||
Install every lexicon NSID that the code uses. Run `lex install` once per NSID (or pass multiple NSIDs at once):
|
||||
|
||||
```sh
|
||||
lex install com.atproto.identity.resolveHandle app.bsky.feed.post
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Some systems (like MacOS) already have a `lex` command. If that is the case, use `npx lex`, `pnpm exec lex` or `yarn lex` to run the correct binary.
|
||||
|
||||
This creates a `manifest.json` and a local `./lexicons/` directory with the schema files the package depends on.
|
||||
|
||||
The `manifest.json` file and `./lexicons/` directory (schema inputs) should be committed to git.
|
||||
|
||||
### Code generation
|
||||
|
||||
Replace the old codegen script with `lex build` as a prebuild step:
|
||||
|
||||
```diff
|
||||
- "codegen": "lex gen-server --yes ./src/lexicon ./lexicons/com/atproto/*/* ./lexicons/app/bsky/*/* ...",
|
||||
+ "prebuild": "lex build --lexicons ./lexicons --clear --indexFile",
|
||||
+ "postinstall": "lex install --ci",
|
||||
```
|
||||
|
||||
The `--indexFile` flag generates an index file that re-exports all root-level namespaces, and `--clear` ensures a clean output directory on each build.
|
||||
|
||||
The `lex install --ci` command will ensure that the `manifest.json` is up to date with the installed lexicons. Using the `postinstall` hook ensures that the command runs after `npm install` or `yarn install`, which ensures lexicon integrity in CI environments.
|
||||
|
||||
The `./src/lexicons/` directory (generated output) should be gitignored since it is regenerated on every build:
|
||||
|
||||
```sh
|
||||
echo '/src/lexicons/' >> .gitignore
|
||||
```
|
||||
|
||||
## 2. Project Code Setup
|
||||
|
||||
After running `lex build`, the new generated code lives in `src/lexicons/` (plural). Import the namespace objects from the index file:
|
||||
|
||||
```typescript
|
||||
import { app, com, chat } from '../lexicons/index.js'
|
||||
```
|
||||
|
||||
Each namespace provides access to schemas through dot notation:
|
||||
|
||||
- `app.bsky.feed.post` - a record schema
|
||||
- `app.bsky.feed.defs.postView` - an object definition
|
||||
- `com.atproto.admin.defs.repoRef` - another object definition
|
||||
- `app.bsky.feed.getAuthorFeed` - a query/procedure schema
|
||||
|
||||
The old codegen used `ids` for NSID string constants (e.g., `ids.AppBskyFeedPost`). These are replaced with `$type` or `$lxm` properties on the schema objects.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> If the app's build process & bundler supports it, consider using path aliases to simplify imports from `src/lexicons/index.js` (e.g., `import { app } from '#lexicons'`).
|
||||
|
||||
### Server Creation
|
||||
|
||||
The `createServer` function now comes from `@atproto/xrpc-server` directly, not from generated code:
|
||||
|
||||
```diff
|
||||
- import { createServer } from './lexicon'
|
||||
+ import { createServer } from '@atproto/xrpc-server'
|
||||
```
|
||||
|
||||
The call signature changes slightly:
|
||||
|
||||
```diff
|
||||
- let server = createServer({
|
||||
+ const server = createServer([], {
|
||||
validateResponse: config.debugMode,
|
||||
payload: { ... },
|
||||
})
|
||||
```
|
||||
|
||||
Note the empty array `[]` as first argument.
|
||||
|
||||
The `Server` type used in handler files also changes:
|
||||
|
||||
```diff
|
||||
- import { Server } from '../../../../lexicon'
|
||||
+ import { Server } from '@atproto/xrpc-server'
|
||||
```
|
||||
|
||||
The server's express router is accessed differently:
|
||||
|
||||
```diff
|
||||
- app.use(server.xrpc.router)
|
||||
+ app.use(server.router)
|
||||
```
|
||||
|
||||
### App Context / Initialization
|
||||
|
||||
Replace `AtpAgent` with `Client` from `@atproto/lex`:
|
||||
|
||||
```diff
|
||||
- import { AtpAgent } from '@atproto/api'
|
||||
+ import { Client } from '@atproto/lex'
|
||||
```
|
||||
|
||||
In context/config files, rename fields:
|
||||
|
||||
```diff
|
||||
- searchAgent: AtpAgent | undefined
|
||||
+ searchClient: Client | undefined
|
||||
```
|
||||
|
||||
Client instantiation:
|
||||
|
||||
```diff
|
||||
- const myServiceAgent = config.serviceUrl
|
||||
- ? new AtpAgent({ service: config.serviceUrl })
|
||||
- : undefined
|
||||
- if (myServiceAgent && config.serviceApiKey) {
|
||||
- myServiceAgent.api.setHeader('authorization', `Bearer ${config.serviceApiKey}`)
|
||||
- }
|
||||
+ const myServiceClient = config.serviceUrl
|
||||
+ ? new Client({
|
||||
+ service: config.serviceUrl,
|
||||
+ headers: config.serviceApiKey
|
||||
+ ? { authorization: `Bearer ${config.serviceApiKey}` }
|
||||
+ : undefined,
|
||||
+ })
|
||||
+ : undefined
|
||||
```
|
||||
|
||||
Headers are passed directly in the `Client` constructor options rather than being set imperatively after construction.
|
||||
|
||||
### Type Aliases File (if applicable)
|
||||
|
||||
The recommended pattern is to import from the generated code directly where needed, using import aliases. However, if the project contains a centralized types file that re-exports types from generated schemas, update it to import from the new generated code.
|
||||
|
||||
```typescript
|
||||
import { app, chat, com } from '../lexicons/index.js'
|
||||
|
||||
// Type aliases
|
||||
export type PostRecord = app.bsky.feed.post.Main
|
||||
export type PostView = app.bsky.feed.defs.PostView
|
||||
export type Label = com.atproto.label.defs.Label
|
||||
export type StrongRef = com.atproto.repo.strongRef.Main
|
||||
|
||||
// Type guard aliases
|
||||
export const isPostRecord = app.bsky.feed.post.$matches
|
||||
export const isImagesEmbed = app.bsky.embed.images.$matches
|
||||
|
||||
// Validation aliases
|
||||
export const parseStrongRef = com.atproto.repo.strongRef.$safeParse
|
||||
```
|
||||
|
||||
The pattern is consistent:
|
||||
|
||||
- **Type**: `export type Foo = namespace.path.TypeName`
|
||||
- **Type guard**: `export const isFoo = namespace.path.$matches`
|
||||
- **Validation**: `export const parseFoo = namespace.path.$safeParse`
|
||||
|
||||
Types for the "main" definition of a record/object use `.Main`, sub-definitions use their specific name (e.g., `.ReplyRef`, `.ViewRecord`).
|
||||
|
||||
## 3. Endpoint Handlers Registration
|
||||
|
||||
The old pattern used method-chain registration on the server object. The new pattern uses `server.add()` with the schema object:
|
||||
|
||||
```diff
|
||||
- server.app.bsky.feed.getAuthorFeed({
|
||||
+ server.add(app.bsky.feed.getAuthorFeed, {
|
||||
auth: ctx.authVerifier.optionalStandardOrRole,
|
||||
handler: async ({ params, auth, req }) => { ... },
|
||||
})
|
||||
```
|
||||
|
||||
```diff
|
||||
- server.com.atproto.identity.resolveHandle(async ({ req, params }) => {
|
||||
+ server.add(com.atproto.identity.resolveHandle, async ({ params }) => {
|
||||
```
|
||||
|
||||
The schema object is always imported from the generated `lexicons/` directory.
|
||||
|
||||
### Handler Return Type Safety
|
||||
|
||||
When handlers return JSON responses, use `'application/json' as const` for the encoding field to satisfy the return type:
|
||||
|
||||
```typescript
|
||||
return {
|
||||
encoding: 'application/json' as const,
|
||||
body: { preferences },
|
||||
}
|
||||
```
|
||||
|
||||
Without the `as const`, TypeScript widens the string literal type and the handler's return type won't match. This is a type-level change only.
|
||||
|
||||
Alternatively, a `satisfies` clause can be used to ensure the returned object matches the expected schema output type:
|
||||
|
||||
```typescript
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: { preferences },
|
||||
} satisfies app.bsky.actor.getPreferences.$Output
|
||||
```
|
||||
|
||||
### LXM References in Handlers
|
||||
|
||||
Inside handlers, access the `$lxm` property from the schema for auth/proxy computations:
|
||||
|
||||
```typescript
|
||||
const lxm = app.bsky.actor.getPreferences.$lxm
|
||||
const aud = computeProxyTo(ctx, req, lxm)
|
||||
permissions.assertRpc({ aud, lxm })
|
||||
```
|
||||
|
||||
### Query/Procedure Parameter Types
|
||||
|
||||
Replace old codegen type imports with `$Params` on the schema:
|
||||
|
||||
```diff
|
||||
- import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed'
|
||||
+ import { app } from '../../../../lexicons/index.js'
|
||||
|
||||
- type Params = QueryParams
|
||||
+ type Params = app.bsky.feed.getAuthorFeed.$Params
|
||||
```
|
||||
|
||||
### Output Types
|
||||
|
||||
Use `$Output` for response type annotations with `satisfies`:
|
||||
|
||||
```typescript
|
||||
return {
|
||||
encoding: 'application/json' as const,
|
||||
body: { actor, relationships },
|
||||
} satisfies app.bsky.graph.getRelationships.$Output
|
||||
```
|
||||
|
||||
### Record/Object Types
|
||||
|
||||
Replace individual type imports with namespace-based access:
|
||||
|
||||
```diff
|
||||
- import { Record as PostRecord } from '../lexicon/types/app/bsky/feed/post'
|
||||
+ // Use the types file or direct namespace access:
|
||||
+ import { PostRecord } from './types.js'
|
||||
+ // or: app.bsky.feed.post.Main
|
||||
```
|
||||
|
||||
### Type Aliases for Defs
|
||||
|
||||
When code uses types from `defs` files, reference them through the namespace:
|
||||
|
||||
```diff
|
||||
- import { StatusAttr } from '../../lexicon/types/com/atproto/admin/defs'
|
||||
+ // Use namespace access:
|
||||
+ type StatusAttr = com.atproto.admin.defs.StatusAttr
|
||||
```
|
||||
|
||||
```diff
|
||||
- type CodeDetail = SomeCustomType
|
||||
+ type CodeDetail = com.atproto.server.defs.InviteCode
|
||||
+ type CodeUse = com.atproto.server.defs.InviteCodeUse
|
||||
```
|
||||
|
||||
Whenever a new object with a `$type` needs to be constructed, use the `$build()` method on the schema:
|
||||
|
||||
```typescript
|
||||
const code = com.atproto.server.defs.inviteCode.$build({
|
||||
code: invite.code,
|
||||
available: invite.availableUses - invite.uses.length,
|
||||
disabled: invite.disabled === 1,
|
||||
forAccount: invite.forUser,
|
||||
createdBy: invite.createdBy,
|
||||
createdAt: invite.createdAt,
|
||||
uses: invite.uses,
|
||||
})
|
||||
```
|
||||
|
||||
Otherwise, for plain data objects that don't require a `$type`, just use the namespace types for type annotations without changing the construction logic:
|
||||
|
||||
```typescript
|
||||
const code: com.atproto.server.defs.InviteCode = {
|
||||
code: invite.code,
|
||||
available: invite.availableUses - invite.uses.length,
|
||||
disabled: invite.disabled === 1,
|
||||
forAccount: invite.forUser,
|
||||
createdBy: invite.createdBy,
|
||||
createdAt: invite.createdAt,
|
||||
uses: invite.uses,
|
||||
}
|
||||
```
|
||||
|
||||
### Token Values
|
||||
|
||||
For accessing lexicon "token" string constants:
|
||||
|
||||
```diff
|
||||
- import { CURATELIST, MODLIST } from '../../../../lexicon/types/app/bsky/graph/defs'
|
||||
+ import { app } from '../../../../lexicons/index.js'
|
||||
+ const CURATELIST = app.bsky.graph.defs.curatelist.value
|
||||
+ const MODLIST = app.bsky.graph.defs.modlist.value
|
||||
```
|
||||
|
||||
### NSID String Constants
|
||||
|
||||
The old `ids` object (e.g., `ids.AppBskyFeedPost`) is replaced by `$type` on the schema:
|
||||
|
||||
```diff
|
||||
- import { ids } from '../../../lexicon/lexicons'
|
||||
- if (uri.collection === ids.AppBskyGraphList) {
|
||||
+ import { app } from '../../../lexicons/index.js'
|
||||
+ if (uri.collection === app.bsky.graph.list.$type) {
|
||||
```
|
||||
|
||||
For LXM (lexicon method) checks in auth:
|
||||
|
||||
```diff
|
||||
- method === ids.AppBskyFeedGetFeedSkeleton
|
||||
+ method === app.bsky.feed.getFeedSkeleton.$lxm
|
||||
```
|
||||
|
||||
For simple comparisons in utility code, plain string literals are also acceptable:
|
||||
|
||||
```diff
|
||||
- if (uri.collection === ids.AppBskyFeedPost) {
|
||||
+ if (uri.collection === 'app.bsky.feed.post') {
|
||||
```
|
||||
|
||||
## 4. XRPC Client Calls
|
||||
|
||||
### Using `Client.xrpc()`
|
||||
|
||||
Replace `AtpAgent` API calls with `Client.xrpc()`:
|
||||
|
||||
```diff
|
||||
- const res = await ctx.suggestionsAgent.api.app.bsky.unspecced.getSuggestionsSkeleton(
|
||||
- { viewer: params.hydrateCtx.viewer, relativeToDid },
|
||||
- { headers: params.headers },
|
||||
- )
|
||||
- return {
|
||||
- suggestedDids: res.data.actors.map((a) => a.did),
|
||||
- headers: res.headers,
|
||||
- }
|
||||
+ const res = await ctx.suggestionsClient.xrpc(
|
||||
+ app.bsky.unspecced.getSuggestionsSkeleton,
|
||||
+ {
|
||||
+ params: { viewer: params.hydrateCtx.viewer, relativeToDid },
|
||||
+ headers: params.headers,
|
||||
+ },
|
||||
+ )
|
||||
+ return {
|
||||
+ suggestedDids: res.body.actors.map((a) => a.did),
|
||||
+ contentLanguage: res.headers.get('content-language') ?? undefined,
|
||||
+ }
|
||||
```
|
||||
|
||||
Key differences:
|
||||
|
||||
- Response data is on `.body` (not `.data`)
|
||||
- Response headers use the standard `Headers` API (`.get()`)
|
||||
- Parameters go in a `params` sub-object
|
||||
- Procedure input goes in an `input` sub-object (not used in this example)
|
||||
|
||||
### Using `Client.call()`
|
||||
|
||||
Calls that only need the response body, and are happy letting any error propagate, can use the simpler `call()` method:
|
||||
|
||||
```typescript
|
||||
const body = await ctx.suggestionsClient.call(
|
||||
app.bsky.unspecced.getSuggestionsSkeleton,
|
||||
// "params" for Queries, "input" for Procedures:
|
||||
{ viewer: params.hydrateCtx.viewer, relativeToDid },
|
||||
// Optional additional options (see API reference):
|
||||
{
|
||||
headers,
|
||||
signal,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Using `xrpcSafe()` for Error Handling
|
||||
|
||||
For calls where you want to handle errors without exceptions, use `xrpcSafe()`:
|
||||
|
||||
```diff
|
||||
- import { AtpAgent } from '@atproto/api'
|
||||
- import { ResponseType, XRPCError } from '@atproto/xrpc'
|
||||
+ import { xrpcSafe } from '@atproto/lex'
|
||||
|
||||
- const agent = new AtpAgent({ service: fgEndpoint })
|
||||
- try {
|
||||
- const result = await agent.api.app.bsky.feed.getFeedSkeleton(
|
||||
- { feed, limit, cursor },
|
||||
- { headers },
|
||||
- )
|
||||
- skeleton = result.data
|
||||
- } catch (err) {
|
||||
- if (err instanceof AppBskyFeedGetFeedSkeleton.UnknownFeedError) { ... }
|
||||
- if (err instanceof XRPCError) {
|
||||
- if (err.status === ResponseType.Unknown) { ... }
|
||||
- if (err.status === ResponseType.InvalidResponse) { ... }
|
||||
- }
|
||||
- throw err
|
||||
- }
|
||||
+ const result = await xrpcSafe(fgEndpoint, app.bsky.feed.getFeedSkeleton, {
|
||||
+ headers,
|
||||
+ params: { feed, limit, cursor },
|
||||
+ })
|
||||
+ if (!result.success) {
|
||||
+ if (result.matchesSchemaErrors()) {
|
||||
+ throw new InvalidRequestError(result.message, result.error)
|
||||
+ }
|
||||
+ if (result.error === 'InternalServerError') { ... }
|
||||
+ if (result.error === 'UpstreamFailure') { ... }
|
||||
+ throw result.reason
|
||||
+ }
|
||||
+ // result.body is the typed response
|
||||
```
|
||||
|
||||
### XrpcError
|
||||
|
||||
Replace `XRPCError` from `@atproto/xrpc` with `XrpcError` from `@atproto/lex`:
|
||||
|
||||
```diff
|
||||
- import { XRPCError } from '@atproto/xrpc'
|
||||
+ import { XrpcError } from '@atproto/lex'
|
||||
```
|
||||
|
||||
## 5. Type Strictness (Branded Types)
|
||||
|
||||
`@atproto/lex` exports branded string types that improve type safety. Apply these at type boundaries — function signatures, interface fields, database schema types — while keeping runtime code unchanged where possible.
|
||||
|
||||
### `DidString`
|
||||
|
||||
```diff
|
||||
- did: string
|
||||
+ did: DidString
|
||||
```
|
||||
|
||||
```diff
|
||||
- iss: string
|
||||
+ iss: DidString | `${DidString}#${string}`
|
||||
```
|
||||
|
||||
Use the type guard instead of string prefix checks:
|
||||
|
||||
```diff
|
||||
- if (typeof iss !== 'string' || !iss.startsWith('did:')) {
|
||||
+ if (typeof iss !== 'string' || !isDidString(iss)) {
|
||||
```
|
||||
|
||||
Import from `@atproto/lex`:
|
||||
|
||||
```typescript
|
||||
import { DidString, isDidString } from '@atproto/lex'
|
||||
```
|
||||
|
||||
### `HandleString`
|
||||
|
||||
```diff
|
||||
- handle: string
|
||||
+ handle: HandleString
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { HandleString, isHandleString } from '@atproto/lex'
|
||||
```
|
||||
|
||||
### `AtIdentifierString`
|
||||
|
||||
For parameters that accept either a DID or a handle:
|
||||
|
||||
```diff
|
||||
- handleOrDid: string
|
||||
+ handleOrDid: AtIdentifierString
|
||||
```
|
||||
|
||||
Use the guard before passing to functions that require a specific type:
|
||||
|
||||
```typescript
|
||||
import { AtIdentifierString, isAtIdentifierString } from '@atproto/lex'
|
||||
|
||||
if (!isAtIdentifierString(actor)) {
|
||||
throw new InvalidRequestError('Invalid actor identifier')
|
||||
}
|
||||
const account = await getAccount(actor)
|
||||
```
|
||||
|
||||
### `AtUriString`
|
||||
|
||||
Apply to URI fields coming from data plane responses:
|
||||
|
||||
```diff
|
||||
- post: { uri: item.uri, cid: item.cid || undefined },
|
||||
+ post: { uri: item.uri as AtUriString, cid: item.cid || undefined },
|
||||
```
|
||||
|
||||
### `Cid`
|
||||
|
||||
Replace `CID` from `multiformats` with `Cid` from `@atproto/lex-data`:
|
||||
|
||||
```diff
|
||||
- import { CID } from 'multiformats/cid'
|
||||
+ import { Cid, parseCid } from '@atproto/lex-data'
|
||||
```
|
||||
|
||||
Or import from `@atproto/lex` directly:
|
||||
|
||||
```typescript
|
||||
import { Cid } from '@atproto/lex'
|
||||
```
|
||||
|
||||
### `DatetimeString`
|
||||
|
||||
For datetime fields in database schemas and interfaces:
|
||||
|
||||
```diff
|
||||
- indexedAt: string
|
||||
+ indexedAt: DatetimeString
|
||||
```
|
||||
|
||||
### Type Narrowing at Data Boundaries
|
||||
|
||||
When data comes from external sources (protobuf, data plane, Kysely queries), cast to branded types at the boundary:
|
||||
|
||||
```diff
|
||||
- suggestedDids: dids,
|
||||
+ suggestedDids: dids as DidString[],
|
||||
```
|
||||
|
||||
```diff
|
||||
- qb.where('actor.did', '=', filter.sub!)
|
||||
+ qb.where('actor.did', '=', filter.sub! as DidString)
|
||||
```
|
||||
|
||||
This pattern is common when Kysely query builders need branded types that the query parameter doesn't naturally have.
|
||||
|
||||
### `HeadersMap`
|
||||
|
||||
Replace `Record<string, string>` headers with proper `Headers` type:
|
||||
|
||||
```diff
|
||||
- import { HeadersMap } from '@atproto/xrpc'
|
||||
+ import { Headers as HeadersMap } from '@atproto/xrpc-server'
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> `Headers` from `@atproto/xrpc-server` conflicts with the standard `Headers` type, so we alias it as `HeadersMap` to avoid confusion.
|
||||
|
||||
Response headers from `xrpc()` calls use the standard `Headers` API:
|
||||
|
||||
```diff
|
||||
- result.headers['content-language']
|
||||
+ result.headers.get('content-language')
|
||||
```
|
||||
|
||||
## 6. Data Utilities
|
||||
|
||||
### JSON/Lex Parsing
|
||||
|
||||
Replace `jsonStringToLex` from `@atproto/lexicon` with `lexParse` from `@atproto/lex`:
|
||||
|
||||
```diff
|
||||
- import { jsonStringToLex } from '@atproto/lexicon'
|
||||
+ import { lexParse } from '@atproto/lex'
|
||||
|
||||
- const parsed = jsonStringToLex(
|
||||
- Buffer.from(payload).toString('utf8'),
|
||||
- ) as SubjectActivitySubscription
|
||||
+ const parsed = lexParse<app.bsky.notification.defs.SubjectActivitySubscription>(
|
||||
+ Buffer.from(payload).toString('utf8'),
|
||||
+ )
|
||||
```
|
||||
|
||||
`lexParse` accepts a type parameter, eliminating the need for `as` casts.
|
||||
|
||||
### Datetime Strings
|
||||
|
||||
Replace `new Date().toISOString()` with branded datetime utilities for AT Protocol datetime fields:
|
||||
|
||||
```diff
|
||||
- createdAt: new Date().toISOString(),
|
||||
+ createdAt: currentDatetimeString(),
|
||||
```
|
||||
|
||||
For converting an existing `Date` object:
|
||||
|
||||
```diff
|
||||
- indexedAt: someDate.toISOString(),
|
||||
+ indexedAt: toDatetimeString(someDate),
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { toDatetimeString, currentDatetimeString } from '@atproto/lex'
|
||||
```
|
||||
|
||||
### BlobRef
|
||||
|
||||
The old `BlobRef` class from `@atproto/lexicon` is replaced by a simple interface from `@atproto/lex-data`. It is no longer a class, so `instanceof` checks are not possible anymore. Instead, use type guards to check if an object is a `BlobRef`:
|
||||
|
||||
```diff
|
||||
- import { BlobRef } from '@atproto/lexicon'
|
||||
+ import { BlobRef } from '@atproto/lex-data'
|
||||
|
||||
- export const cidFromBlobJson = (json: BlobRef) => {
|
||||
- if (json instanceof BlobRef) {
|
||||
- return json.ref.toString()
|
||||
- }
|
||||
- if (json['$type'] === 'blob') {
|
||||
- return (json['ref']?.['$link'] ?? '') as string
|
||||
- }
|
||||
- return (json['cid'] ?? '') as string
|
||||
- }
|
||||
+ export const cidFromBlobJson = (json: BlobRef): string => {
|
||||
+ return json.ref.toString()
|
||||
+ }
|
||||
```
|
||||
|
||||
```diff
|
||||
- if (value instanceof BlobRef) { ... }
|
||||
+ if (isBlobRef(value)) { ... }
|
||||
```
|
||||
|
||||
### Legacy BlobRefs
|
||||
|
||||
Legacy blob references (`{ cid: string, mimeType: string }`) are automatically handled based on the **strict mode** setting. When `strict: false`, both standard and legacy blob formats are accepted. When `strict: true` (the default), only standard `TypedBlobRef` format is accepted.
|
||||
|
||||
```typescript
|
||||
import {
|
||||
TypedBlobRef,
|
||||
LegacyBlobRef,
|
||||
isTypedBlobRef,
|
||||
isLegacyBlobRef,
|
||||
} from '@atproto/lex-data'
|
||||
|
||||
// Check for standard BlobRef
|
||||
if (isTypedBlobRef(value)) {
|
||||
console.log(value.ref.toString())
|
||||
}
|
||||
|
||||
// Check for legacy format
|
||||
if (isLegacyBlobRef(value)) {
|
||||
console.log(value.cid)
|
||||
}
|
||||
```
|
||||
|
||||
New utility functions are available for working with both formats:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
BlobRef,
|
||||
getBlobCid,
|
||||
getBlobCidString,
|
||||
getBlobMime,
|
||||
getBlobSize,
|
||||
} from '@atproto/lex-data'
|
||||
|
||||
declare const blobRef: BlobRef // TypedBlobRef | LegacyBlobRef
|
||||
|
||||
const cid = getBlobCid(blobRef) // Returns Cid object
|
||||
const cidString = getBlobCidString(blobRef) // Returns string (optimized)
|
||||
const mimeType = getBlobMime(blobRef)
|
||||
const size = getBlobSize(blobRef) // Returns number | undefined (legacy refs don't have size)
|
||||
```
|
||||
|
||||
### Strict Mode in Validation
|
||||
|
||||
All schema validation methods (`$parse`, `$safeParse`, `$validate`, `$safeValidate`) accept an optional `{ strict }` option that controls validation behavior uniformly across both parse and validate modes:
|
||||
|
||||
**Strict mode (`strict: true`, the default):**
|
||||
|
||||
- Datetime strings must have proper timezone information
|
||||
- Blob MIME types and size constraints are enforced
|
||||
- Only raw CIDs are allowed in blob references
|
||||
- Legacy blob references are rejected
|
||||
|
||||
**Non-strict mode (`strict: false`):**
|
||||
|
||||
- Datetime strings without timezones are accepted
|
||||
- Blob MIME type and size constraints are not enforced
|
||||
- Any valid CID is allowed in blob references
|
||||
- Legacy blob references are accepted
|
||||
|
||||
```typescript
|
||||
// Default strict validation
|
||||
const result1 = schema.$safeParse(data) // strict: true by default
|
||||
|
||||
// Explicit strict validation
|
||||
const result2 = schema.$safeParse(data, { strict: true })
|
||||
|
||||
// Non-strict validation (lenient)
|
||||
const result3 = schema.$safeParse(data, { strict: false })
|
||||
|
||||
// Applies to all validation methods
|
||||
schema.$validate(data, { strict: false })
|
||||
schema.$parse(data, { strict: false })
|
||||
schema.$safeValidate(data, { strict: false })
|
||||
```
|
||||
|
||||
The `Client` class has a `strictResponseProcessing` option that controls the default strict mode for all XRPC calls:
|
||||
|
||||
```typescript
|
||||
const client = new Client(session, {
|
||||
strictResponseProcessing: false, // Use non-strict mode for all calls
|
||||
})
|
||||
```
|
||||
|
||||
When `strictResponseProcessing: false`, response validation will use `strict: false`, which means legacy blobs and other lenient data formats are automatically accepted. Individual calls can override this with per-call options.
|
||||
|
||||
### Lex Stringify
|
||||
|
||||
```diff
|
||||
- import { stringifyLex } from '@atproto/lexicon'
|
||||
+ import { lexStringify } from '@atproto/lex'
|
||||
```
|
||||
|
||||
## 7. Schema Validation and Type Guards
|
||||
|
||||
### `$matches()` — Validates and Narrows Unknown Data
|
||||
|
||||
Use `$matches()` when the data has not been pre-validated (e.g., it comes from an external source, or you need full runtime validation):
|
||||
|
||||
```diff
|
||||
- import { isRepoRef } from '../../../../lexicon/types/com/atproto/admin/defs'
|
||||
- if (isRepoRef(subject)) { ... }
|
||||
+ if (com.atproto.admin.defs.repoRef.$matches(subject)) { ... }
|
||||
```
|
||||
|
||||
```diff
|
||||
- repost: isSkeletonReasonRepost(item.reason) ? ... : undefined,
|
||||
+ repost: app.bsky.feed.defs.skeletonReasonRepost.$matches(item.reason) ? ... : undefined,
|
||||
```
|
||||
|
||||
### `$isTypeOf` — Discriminates Pre-Validated Data by `$type`
|
||||
|
||||
Use `$isTypeOf` when the data is already validated and you only need to discriminate based on the `$type` property. This is faster than `$matches()` because it skips validation. Common in `.find()` and `.filter()` callbacks on arrays of already-parsed preference objects or union members:
|
||||
|
||||
```diff
|
||||
- const personalDetailsPref = prefs.find(
|
||||
- (pref) => pref.$type === 'app.bsky.actor.defs#personalDetailsPref'
|
||||
- )
|
||||
+ const personalDetailsPref = prefs.find(
|
||||
+ app.bsky.actor.defs.personalDetailsPref.$isTypeOf,
|
||||
+ )
|
||||
```
|
||||
|
||||
`$isTypeOf` is a type predicate function, so TypeScript narrows the type automatically when used in conditionals or `.find()`.
|
||||
|
||||
### `$build()` — Constructs Typed Objects
|
||||
|
||||
Use `$build()` instead of manually setting `$type`:
|
||||
|
||||
```diff
|
||||
- return {
|
||||
- $type: 'app.bsky.graph.defs#relationship',
|
||||
- did,
|
||||
- following: subject.following,
|
||||
- }
|
||||
+ return app.bsky.graph.defs.relationship.$build({
|
||||
+ did,
|
||||
+ following: subject.following,
|
||||
+ })
|
||||
```
|
||||
|
||||
```diff
|
||||
- prefs.push({
|
||||
- $type: 'app.bsky.actor.defs#declaredAgePref',
|
||||
- isOverAge13: age >= 13,
|
||||
- isOverAge16: age >= 16,
|
||||
- isOverAge18: age >= 18,
|
||||
- })
|
||||
+ prefs.push(
|
||||
+ app.bsky.actor.defs.declaredAgePref.$build({
|
||||
+ isOverAge13: age >= 13,
|
||||
+ isOverAge16: age >= 16,
|
||||
+ isOverAge18: age >= 18,
|
||||
+ }),
|
||||
+ )
|
||||
```
|
||||
|
||||
`$build()` automatically sets the `$type` field and returns a properly typed object.
|
||||
|
||||
## Tests
|
||||
|
||||
Tests still rely exclusively on `@atproto/api`. When tests previously imported from the old `src/lexicon/` directory, redirect those imports to `@atproto/api`:
|
||||
|
||||
```diff
|
||||
- import { ids } from '../../src/lexicon/lexicons'
|
||||
- import { RepoRef, isRepoRef } from '../../src/lexicon/types/com/atproto/admin/defs'
|
||||
- import { $Typed } from '../../src/lexicon/util'
|
||||
+ import { $Typed, AtpAgent, ComAtprotoAdminDefs, ids } from '@atproto/api'
|
||||
```
|
||||
|
||||
Do not change how tests make XRPC calls — they continue to use `AtpAgent` from `@atproto/api`. This allows to ensure that the refactor does not break existing functionality at runtime. Tests will be migrated to `@atproto/lex` in a separate phase after all service code has been lexified.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **`$type` vs `$lxm`**: Use `$type` for record/object type strings (e.g., `app.bsky.feed.post.$type` = `'app.bsky.feed.post'`). Use `$lxm` for XRPC method identifiers used in auth checks and proxy routing. `$nsid` is also available for the raw NSID string if needed, this is especially useful for lexicon defs that don't have a `main` type but still need to reference their NSID.
|
||||
|
||||
2. **`$matches` vs `$isTypeOf`**: Use `$matches()` when data needs validation (unknown input). Use `$isTypeOf` when data is already validated and you just need to check the `$type` tag (e.g., union discrimination, filtering an array of pre-parsed objects).
|
||||
|
||||
3. **Branded type casts at boundaries**: Data from protobuf/data plane/Kysely returns plain strings. Cast to branded types (`as DidString`, `as AtUriString`) at the point where data enters the typed domain. Avoid `assert()` — use `as` casts at known-safe boundaries instead.
|
||||
|
||||
4. **`'application/json' as const`**: Handler return values need `as const` on the encoding string literal to satisfy the return type. Without it, TypeScript widens the type.
|
||||
|
||||
5. **Response header changes**: Old `AtpAgent` returned headers as a plain object with string indexing. New `Client`/`xrpc` returns standard `Headers` objects requiring `.get()`.
|
||||
|
||||
6. **`@atproto/lex-data` sub-export**: `Cid`, `parseCid`, and `BlobRef` are available from `@atproto/lex-data` for files that only need data types without the full `@atproto/lex` package. Both import paths work.
|
||||
|
||||
7. **Prefer `@atproto/lex` imports** over `@atproto/syntax` when both export the same symbol (e.g., `DidString`, `AtUriString`).
|
||||
|
||||
8. **Avoid `assert()` calls.** Use type guards (`isDidString()`, `isHandleString()`) with conditional logic rather than assertions.
|
||||
|
||||
## Import Source Changes Summary
|
||||
|
||||
| Before | After |
|
||||
| ------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| `@atproto/api` (`AtpAgent`) | `@atproto/lex` (`Client`) |
|
||||
| `@atproto/lexicon` (`jsonStringToLex`, `BlobRef`) | `@atproto/lex` (`lexParse`, `BlobRef`) |
|
||||
| `@atproto/lexicon` (`stringifyLex`) | `@atproto/lex` (`lexStringify`) |
|
||||
| `@atproto/xrpc` (`HeadersMap`, `XRPCError`) | `@atproto/xrpc-server` (`Headers`), `@atproto/lex` (`XrpcError`, `xrpcSafe`) |
|
||||
| `multiformats/cid` (`CID`) | `@atproto/lex` (`Cid`, `parseCid`) |
|
||||
| `@atproto/syntax` (`DidString`, etc.) | `@atproto/lex` (`DidString`, `HandleString`, etc.) — prefer `@atproto/lex` |
|
||||
| `../lexicon` (`Server`, `createServer`) | `@atproto/xrpc-server` (`Server`, `createServer`) |
|
||||
| `../lexicon/lexicons` (`ids`) | `../lexicons/index.js` (`app`, `com`, `chat`) |
|
||||
| `../lexicon/types/...` (types, guards) | `../lexicons/index.js` (namespace-qualified access) |
|
||||
@@ -0,0 +1,247 @@
|
||||
---
|
||||
name: vitest-patterns
|
||||
description: Patterns and conventions for writing vitest tests in this project. This skill should be used when writing new tests, adding test cases to existing test files, or reviewing test code for correctness. Trigger whenever the user asks to write tests, add test coverage, create test files, or mentions vitest/testing.
|
||||
---
|
||||
|
||||
# Vitest Test Patterns
|
||||
|
||||
Conventions and patterns for writing vitest tests in this codebase.
|
||||
|
||||
## Imports
|
||||
|
||||
Always import test utilities from `vitest`. Use named imports:
|
||||
|
||||
```ts
|
||||
import { assert, describe, expect, it, vi } from 'vitest'
|
||||
```
|
||||
|
||||
Only import what you use. Add `vi` only when using mock functions. Add `assert` only when narrowing types.
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Describe labels
|
||||
|
||||
Pass the function/class under test directly as the `describe` label when possible. Use string labels for conceptual groupings:
|
||||
|
||||
```ts
|
||||
// Function reference as label (preferred when testing a single export)
|
||||
describe(parseCid, () => { ... })
|
||||
describe(isLexValue, () => { ... })
|
||||
|
||||
// String label for conceptual groups or when testing multiple related behaviors
|
||||
describe('roundtrip toBase64 <-> fromBase64', () => { ... })
|
||||
describe('isObject', () => { ... })
|
||||
```
|
||||
|
||||
### Grouping
|
||||
|
||||
Nest logical groupings inside the top-level describe. Common groupings:
|
||||
|
||||
```ts
|
||||
describe(someFunction, () => {
|
||||
describe('valid inputs', () => { ... })
|
||||
describe('invalid inputs', () => { ... })
|
||||
describe('edge cases', () => { ... })
|
||||
})
|
||||
```
|
||||
|
||||
For features with a default behavior and an override, cover both:
|
||||
|
||||
```ts
|
||||
describe('validateResponse', () => {
|
||||
it('rejects invalid response body by default', ...)
|
||||
it('accepts invalid response body when disabled', ...)
|
||||
it('succeeds with valid response body when enabled', ...)
|
||||
})
|
||||
```
|
||||
|
||||
For safety-critical code, group edge cases under a `'safety'` or `'edge cases'` describe:
|
||||
|
||||
```ts
|
||||
describe('safety', () => {
|
||||
it('handles cyclic structures without infinite loops', () => { ... })
|
||||
it('handles deep structures without exceeding call stack', () => { ... })
|
||||
})
|
||||
```
|
||||
|
||||
## Parameterized Tests
|
||||
|
||||
Use `for...of` loops over test case arrays instead of `it.each`:
|
||||
|
||||
```ts
|
||||
describe(isLexScalar, () => {
|
||||
for (const { note, value, expected } of [
|
||||
{ note: 'string', value: 'hello', expected: true },
|
||||
{ note: 'boolean', value: true, expected: true },
|
||||
{ note: 'null', value: null, expected: true },
|
||||
{ note: 'number (float)', value: 3.14, expected: false },
|
||||
{ note: 'undefined', value: undefined, expected: false },
|
||||
]) {
|
||||
it(note, () => {
|
||||
expect(isLexScalar(value)).toBe(expected)
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
This also works for running the same test suite against multiple implementations:
|
||||
|
||||
```ts
|
||||
for (const utf8Len of [utf8LenNode!, utf8LenCompute!] as const) {
|
||||
describe(utf8Len, () => {
|
||||
it('computes utf8 string length', () => {
|
||||
expect(utf8Len('a')).toBe(1)
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Assertions
|
||||
|
||||
### Type narrowing with `assert`
|
||||
|
||||
Use `assert()` from vitest for type narrowing and boolean checks. **Always prefer `assert(result.success)` over `expect(result.success).toBe(true)`** — the `assert` provides type narrowing in TypeScript, which allows the rest of the test to access narrowed properties without additional type guards.
|
||||
|
||||
```ts
|
||||
// Narrow to a specific type before further assertions
|
||||
assert(err instanceof XrpcFetchError)
|
||||
expect(err.cause).toBeInstanceOf(TypeError)
|
||||
|
||||
// Discriminated union checks - PREFERRED
|
||||
assert(result.success)
|
||||
expect(result.body).toEqual({ value: 'hello' })
|
||||
// TypeScript now knows result.body exists
|
||||
|
||||
assert(!result.success)
|
||||
expect(result).toBeInstanceOf(XrpcResponseError)
|
||||
// TypeScript now knows result has error properties
|
||||
|
||||
// DON'T do this - it doesn't narrow types
|
||||
expect(result.success).toBe(true) // ❌ No type narrowing
|
||||
if (result.success) {
|
||||
expect(result.body).toEqual({ value: 'hello' }) // Still need type guard
|
||||
}
|
||||
```
|
||||
|
||||
### Error assertions with `rejects.toSatisfy`
|
||||
|
||||
For thrown errors, prefer `rejects.toSatisfy()` over `rejects.toThrow()` when you need multiple detailed assertions:
|
||||
|
||||
```ts
|
||||
await expect(
|
||||
someAsyncFn(),
|
||||
).rejects.toSatisfy((err) => {
|
||||
assert(err instanceof SomeError)
|
||||
expect(err.cause).toBeInstanceOf(TypeError)
|
||||
expect(err.message).toContain('failed')
|
||||
return true // must return true
|
||||
})
|
||||
```
|
||||
|
||||
Always `return true` at the end of `toSatisfy` callbacks.
|
||||
|
||||
For simple "it throws" checks, `toThrow()` is fine:
|
||||
|
||||
```ts
|
||||
expect(() => parseCid(invalidStr)).toThrow()
|
||||
expect(() => cidForRawHash(new Uint8Array(31))).toThrow('Invalid SHA-256 hash length')
|
||||
```
|
||||
|
||||
## Mock Functions
|
||||
|
||||
### Use `vi.fn()` with type parameters
|
||||
|
||||
When you need to inspect how a function was called, use `vi.fn<Type>()`:
|
||||
|
||||
```ts
|
||||
const fetchHandler = vi.fn<FetchHandler>(async () =>
|
||||
Response.json({ value: 'ok' }),
|
||||
)
|
||||
|
||||
await xrpc(fetchHandler, testQuery, { params: { limit: 25 } })
|
||||
|
||||
expect(fetchHandler).toHaveBeenCalledOnce()
|
||||
const [path, init] = fetchHandler.mock.calls[0]
|
||||
expect(path).toContain('/xrpc/io.example.testQuery')
|
||||
```
|
||||
|
||||
When you don't need to inspect calls, use a plain typed function:
|
||||
|
||||
```ts
|
||||
const fetchHandler: FetchHandler = async () => Response.json({ value: 'hello' })
|
||||
```
|
||||
|
||||
## Test Fixtures
|
||||
|
||||
Define reusable fixtures at the top of the file, outside describe blocks:
|
||||
|
||||
```ts
|
||||
const invalidCidStr = 'invalidcidstring'
|
||||
const cborCidStr = 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a'
|
||||
const cborCid = parseCid(cborCidStr, { flavor: 'cbor' })
|
||||
```
|
||||
|
||||
Keep fixtures minimal and focused on what the tests need.
|
||||
|
||||
## TypeScript in Tests
|
||||
|
||||
### Intentionally invalid arguments
|
||||
|
||||
Use `// @ts-expect-error` with a description:
|
||||
|
||||
```ts
|
||||
await xrpc(fetchHandler, testQuery, {
|
||||
// @ts-expect-error intentionally passing invalid params
|
||||
params: { limit: 'not-a-number' },
|
||||
validateRequest: true,
|
||||
})
|
||||
```
|
||||
|
||||
## Global Stubbing
|
||||
|
||||
When testing code that uses a global, temporarily replace it and restore in a `finally` block:
|
||||
|
||||
```ts
|
||||
it('throws TypeError when fetch is not available', () => {
|
||||
const originalFetch = globalThis.fetch
|
||||
try {
|
||||
// @ts-expect-error removing fetch to simulate missing environment
|
||||
globalThis.fetch = undefined
|
||||
expect(() => buildAgent({ service: 'https://example.com' })).toThrow(TypeError)
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Use `try/finally` (not `beforeEach`/`afterEach`) when the stub is scoped to a single test.
|
||||
|
||||
## Roundtrip Tests
|
||||
|
||||
When testing encode/decode or serialize/deserialize pairs, add a dedicated roundtrip describe:
|
||||
|
||||
```ts
|
||||
describe('roundtrip toBase64 <-> fromBase64', () => {
|
||||
it('roundtrips empty array', () => {
|
||||
const original = new Uint8Array(0)
|
||||
expect(ui8Equals(fromBase64(toBase64(original)), original)).toBe(true)
|
||||
})
|
||||
|
||||
it('roundtrips all byte values', () => {
|
||||
const allBytes = new Uint8Array(256)
|
||||
for (let i = 0; i < 256; i++) allBytes[i] = i
|
||||
expect(ui8Equals(fromBase64(toBase64(allBytes)), allBytes)).toBe(true)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
- Do not worry about code formatting or lint issues. The user will review changes in their editor, which applies formatting automatically on save.
|
||||
- Do not commit test changes. Leave them as unstaged modifications for the user to review.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
pnpm exec vitest run path/to/file.test.ts
|
||||
```
|
||||
@@ -1,4 +1,5 @@
|
||||
node_modules
|
||||
**/dist
|
||||
.DS_Store
|
||||
.git
|
||||
Dockerfile
|
||||
|
||||
+22
-3
@@ -1,5 +1,24 @@
|
||||
dist
|
||||
node_modules
|
||||
|
||||
# buf
|
||||
packages/bsky/src/proto
|
||||
packages/bsync/src/proto
|
||||
|
||||
# codegen
|
||||
packages/api/src/client
|
||||
packages/lexicon-resolver/src/client
|
||||
packages/bsky/src/lexicon
|
||||
packages/pds/src/lexicon
|
||||
packages/ozone/src/lexicon
|
||||
|
||||
# @atproto/lex
|
||||
packages/lexicon-resolver/src/lexicons
|
||||
packages/lex/*/src/lexicons
|
||||
packages/lex/*/tests/lexicons
|
||||
packages/oauth/oauth-client-browser-example/src/lexicons
|
||||
packages/pds/src/lexicons
|
||||
packages/bsky/src/lexicons
|
||||
packages/sync/src/lexicons
|
||||
|
||||
# others
|
||||
packages/api/src/moderation/const/labels.ts
|
||||
packages/oauth/*/src/locales/*/messages.ts
|
||||
packages/oauth/oauth-client-expo/android/build
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"import/extensions": ["off", "ignorePackages"],
|
||||
"import/export": "off",
|
||||
"import/namespace": "off",
|
||||
"import/no-deprecated": "error",
|
||||
"import/no-deprecated": "off",
|
||||
"import/no-absolute-path": "error",
|
||||
"import/no-dynamic-require": "error",
|
||||
"import/no-self-import": "error",
|
||||
@@ -33,6 +33,9 @@
|
||||
"distinctGroup": true,
|
||||
"alphabetize": { "order": "asc" },
|
||||
"newlines-between": "never",
|
||||
"pathGroups": [
|
||||
{ "pattern": "#/**", "group": "parent", "position": "before" }
|
||||
],
|
||||
"groups": [
|
||||
"builtin",
|
||||
"external",
|
||||
@@ -71,7 +74,7 @@
|
||||
"env": { "jest": true }
|
||||
},
|
||||
{
|
||||
"files": "*.js",
|
||||
"files": ["*.js", "*.cjs"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": "off"
|
||||
}
|
||||
@@ -94,6 +97,7 @@
|
||||
"typescript": {
|
||||
"project": [
|
||||
"tsconfig.json",
|
||||
"packages/lex/*/tsconfig.json",
|
||||
"packages/oauth/*/tsconfig.json",
|
||||
"packages/oauth/*/tsconfig.src.json",
|
||||
"packages/internal/*/tsconfig.json",
|
||||
|
||||
+9
-3
@@ -1,7 +1,13 @@
|
||||
packages/**/src/lexicon/** linguist-generated=true
|
||||
packages/api/src/client/** linguist-generated=true
|
||||
packages/lexicon-resolver/src/client/** linguist-generated=true
|
||||
# buf
|
||||
packages/bsky/src/proto/** linguist-generated=true
|
||||
packages/bsync/src/proto/** linguist-generated=true
|
||||
|
||||
# codegen
|
||||
packages/api/src/client/** linguist-generated=true
|
||||
packages/ozone/src/lexicon/** linguist-generated=true
|
||||
|
||||
# @atproto/lex
|
||||
packages/lexicon-resolver/src/lexicons/** linguist-generated=true
|
||||
|
||||
# i18n
|
||||
packages/oauth/oauth-provider-ui/src/locales/**/messages.po linguist-generated=true
|
||||
|
||||
@@ -3,6 +3,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- msi/pds-lexification
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
|
||||
@@ -3,7 +3,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- divy/etcd-dp-host-list
|
||||
- msi/pds-lexification
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
USERNAME: ${{ github.actor }}
|
||||
|
||||
@@ -3,6 +3,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
|
||||
@@ -3,6 +3,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
USERNAME: ${{ github.actor }}
|
||||
|
||||
@@ -3,7 +3,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- divy/ozone-metadata
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
|
||||
@@ -3,6 +3,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
USERNAME: ${{ github.actor }}
|
||||
|
||||
@@ -3,6 +3,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- msi/pds-lexification
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
|
||||
@@ -3,7 +3,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- divy/pds-build-dblock-fix
|
||||
- msi/pds-lexification
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
USERNAME: ${{ github.actor }}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
|
||||
# NOTE(sfn): we can add a custom system prompt here
|
||||
|
||||
claude_args: |
|
||||
--model claude-opus-4-5-20251101
|
||||
@@ -5,33 +5,40 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Publish
|
||||
name: Publish
|
||||
if: github.repository == 'bluesky-social/atproto'
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: corepack enable && corepack prepare --activate
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: 'pnpm'
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- run: pnpm verify
|
||||
cache: pnpm
|
||||
node-version-file: '.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
# Temporary workaround for an issue with Node.js v22
|
||||
# https://github.com/npm/cli/issues/9151
|
||||
# https://github.com/npm/cli/pull/9152
|
||||
- run: npm install --global npm@11.11.1
|
||||
- run: npm install --global npm@latest
|
||||
- run: pnpm install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: true
|
||||
- name: Publish
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
id: changesets
|
||||
with:
|
||||
publish: pnpm release
|
||||
version: pnpm version-packages
|
||||
version: pnpm run version-packages
|
||||
commit: 'Version packages'
|
||||
title: 'Version packages'
|
||||
|
||||
+67
-56
@@ -1,4 +1,4 @@
|
||||
name: Test
|
||||
name: Repository CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -12,95 +12,106 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# https://github.com/actions/setup-node/issues/531#issuecomment-2960522861
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Configure Dependency Cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
- name: Get current month
|
||||
run: echo "CURRENT_MONTH=$(date +'%Y-%m')" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v4
|
||||
name: Cache Puppeteer browser binaries
|
||||
with:
|
||||
path: ~/.cache
|
||||
key: ${{ env.CURRENT_MONTH }}-${{ runner.os }}
|
||||
- run: pnpm i --frozen-lockfile
|
||||
cache: pnpm
|
||||
node-version-file: '.nvmrc'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: true
|
||||
|
||||
- run: pnpm build
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
retention-days: 2
|
||||
path: |
|
||||
packages/*/dist
|
||||
packages/*/*/dist
|
||||
packages/lex/*/src/lexicons
|
||||
packages/lex/*/tests/lexicons
|
||||
packages/oauth/oauth-client-browser-example/src/lexicons
|
||||
packages/oauth/*/src/locales/*/messages.ts
|
||||
retention-days: 1
|
||||
packages/api/src/moderation/const/labels.ts
|
||||
packages/bsky/src/lexicons
|
||||
packages/pds/src/lexicons
|
||||
packages/sync/src/lexicons
|
||||
|
||||
changeset:
|
||||
name: Changeset
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # needed for git diff against base branch
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
cache: pnpm
|
||||
node-version-file: '.nvmrc'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: true
|
||||
- run: pnpm changeset status --since=origin/${{ github.base_ref }}
|
||||
|
||||
test:
|
||||
name: Test
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
# Puppeteer does not work in recent Ubuntu versions without a workaround due
|
||||
# to sandboxing issues. Using "ubuntu-latest" results in the following
|
||||
# error:
|
||||
#
|
||||
# No usable sandbox! If you are running on Ubuntu 23.10+ or another Linux
|
||||
# distro that has disabled unprivileged user namespaces with AppArmor, see
|
||||
# https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md.
|
||||
# Otherwise see
|
||||
# https://chromium.googlesource.com/chromium/src/+/main/docs/linux/suid_sandbox_development.md
|
||||
# for more information on developing with the (older) SUID sandbox. If you
|
||||
# want to live dangerously and need an immediate workaround, you can try
|
||||
# using --no-sandbox.
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# https://github.com/actions/setup-node/issues/531#issuecomment-2960522861
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Configure Dependency Cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
- name: Get current month
|
||||
run: echo "CURRENT_MONTH=$(date +'%Y-%m')" >> $GITHUB_ENV
|
||||
cache: pnpm
|
||||
node-version-file: '.nvmrc'
|
||||
- run: echo "CURRENT_MONTH=$(date +'%Y-%m')" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v4
|
||||
name: Cache Puppeteer browser binaries
|
||||
with:
|
||||
path: ~/.cache
|
||||
key: ${{ env.CURRENT_MONTH }}-${{ runner.os }}
|
||||
- run: pnpm i --frozen-lockfile
|
||||
path: ~/.cache/puppeteer
|
||||
key: ${{ env.CURRENT_MONTH }}-${{ runner.os }}-${{ runner.arch }}
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: packages
|
||||
- run: pnpm test:withFlags --maxWorkers=1 --shard=${{ matrix.shard }} --passWithNoTests
|
||||
|
||||
verify:
|
||||
name: Verify
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# https://github.com/actions/setup-node/issues/531#issuecomment-2960522861
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Configure Dependency Cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
- name: Get current month
|
||||
run: echo "CURRENT_MONTH=$(date +'%Y-%m')" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v4
|
||||
name: Cache Puppeteer browser binaries
|
||||
with:
|
||||
path: ~/.cache
|
||||
key: ${{ env.CURRENT_MONTH }}-${{ runner.os }}
|
||||
- run: pnpm i --frozen-lockfile
|
||||
cache: pnpm
|
||||
node-version-file: '.nvmrc'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: true
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
|
||||
@@ -15,3 +15,5 @@ test.sqlite
|
||||
\#*\#
|
||||
*~
|
||||
*.swp
|
||||
.claude/
|
||||
coverage
|
||||
|
||||
+17
-6
@@ -7,13 +7,24 @@ pnpm-lock.yaml
|
||||
.changeset
|
||||
CHANGELOG.md
|
||||
|
||||
# Prettier is used to format the code during codegen
|
||||
# buf
|
||||
packages/bsky/src/proto
|
||||
packages/bsync/src/proto
|
||||
|
||||
# codegen
|
||||
packages/api/src/client
|
||||
packages/lexicon-resolver/src/client
|
||||
packages/bsky/src/lexicon
|
||||
packages/pds/src/lexicon
|
||||
packages/ozone/src/lexicon
|
||||
|
||||
# Automatically generated by lingui
|
||||
# @atproto/lex
|
||||
packages/lexicon-resolver/src/lexicons
|
||||
packages/lex/*/src/lexicons
|
||||
packages/lex/*/tests/lexicons
|
||||
packages/oauth/oauth-client-browser-example/src/lexicons
|
||||
packages/pds/src/lexicons
|
||||
packages/bsky/src/lexicons
|
||||
packages/sync/src/lexicons
|
||||
|
||||
# others
|
||||
packages/api/src/moderation/const/labels.ts
|
||||
packages/oauth/*/src/locales/*/messages.ts
|
||||
packages/oauth/oauth-provider-frontend/src/routeTree.gen.ts
|
||||
packages/oauth/oauth-client-expo/android/build
|
||||
|
||||
Vendored
+2
-1
@@ -3,6 +3,7 @@
|
||||
"dbaeumer.vscode-eslint",
|
||||
"wengerk.highlight-bad-chars",
|
||||
"esbenp.prettier-vscode",
|
||||
"streetsidesoftware.code-spell-checker"
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"vitest.explorer"
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"cSpell.language": "en,en-US",
|
||||
"cSpell.userWords": [
|
||||
"cSpell.words": [
|
||||
"algs",
|
||||
"appview",
|
||||
"atproto",
|
||||
@@ -8,6 +8,7 @@
|
||||
"bluesky",
|
||||
"bsky",
|
||||
"bsync",
|
||||
"cbor",
|
||||
"clsx",
|
||||
"consolas",
|
||||
"dpop",
|
||||
@@ -16,6 +17,7 @@
|
||||
"hexeditor",
|
||||
"ingester",
|
||||
"insertable",
|
||||
"ipld",
|
||||
"jwks",
|
||||
"keypair",
|
||||
"kysely",
|
||||
|
||||
@@ -23,3 +23,5 @@ ATProto receives so many contributions that we could never list everyone who des
|
||||
#### [daniel](https://hackerone.com/daniel), Security disclosure, November 2024
|
||||
|
||||
#### [imax](https://github.com/imax9000), Security disclosure, January 2025
|
||||
|
||||
#### [avivkeller](https://github.com/avivkeller), Security disclosure, December 2025
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
Dual MIT/Apache-2.0 License
|
||||
|
||||
Copyright (c) 2022-2025 Bluesky Social PBC, and Contributors
|
||||
Copyright (c) 2022-2026 Bluesky Social PBC, and Contributors
|
||||
|
||||
Except as otherwise noted in individual files, this software is licensed under the MIT license (<http://opensource.org/licenses/MIT>), or the Apache License, Version 2.0 (<http://www.apache.org/licenses/LICENSE-2.0>).
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ help: ## Print info about all commands
|
||||
@echo "NOTE: dependencies between commands are not automatic. Eg, you must run 'deps' and 'build' first, and after any changes"
|
||||
|
||||
.PHONY: build
|
||||
build: ## Compile all modules
|
||||
build: codegen ## Compile all modules
|
||||
pnpm build
|
||||
|
||||
.PHONY: test
|
||||
@@ -46,6 +46,10 @@ fmt-lexicons: ## Run syntax re-formatting, just on .json files
|
||||
deps: ## Installs dependent libs using 'pnpm install'
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## Deletes all 'dist' and 'node_package' directories (including nested)
|
||||
rm -rf **/dist **/node_packages
|
||||
|
||||
.PHONY: nvm-setup
|
||||
nvm-setup: ## Use NVM to install and activate node+pnpm
|
||||
nvm install 18
|
||||
|
||||
@@ -113,3 +113,5 @@ This project is dual-licensed under MIT and Apache 2.0 terms:
|
||||
- Apache License, Version 2.0, ([LICENSE-APACHE.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-APACHE.txt) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Downstream projects and end users may chose either license individually, or both together, at their discretion. The motivation for this dual-licensing is the additional software patent assurance provided by Apache 2.0.
|
||||
|
||||
Bluesky Social PBC has committed to a software patent non-aggression pledge. For details see [the original announcement](https://bsky.social/about/blog/10-01-2025-patent-pledge).
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"maxGraphemes": 64,
|
||||
"maxLength": 640
|
||||
},
|
||||
"pronouns": { "type": "string" },
|
||||
"avatar": { "type": "string", "format": "uri" },
|
||||
"associated": {
|
||||
"type": "ref",
|
||||
@@ -31,6 +32,10 @@
|
||||
"status": {
|
||||
"type": "ref",
|
||||
"ref": "#statusView"
|
||||
},
|
||||
"debug": {
|
||||
"type": "unknown",
|
||||
"description": "Debug information for internal development"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -45,6 +50,7 @@
|
||||
"maxGraphemes": 64,
|
||||
"maxLength": 640
|
||||
},
|
||||
"pronouns": { "type": "string" },
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxGraphemes": 256,
|
||||
@@ -69,6 +75,10 @@
|
||||
"status": {
|
||||
"type": "ref",
|
||||
"ref": "#statusView"
|
||||
},
|
||||
"debug": {
|
||||
"type": "unknown",
|
||||
"description": "Debug information for internal development"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -88,6 +98,8 @@
|
||||
"maxGraphemes": 256,
|
||||
"maxLength": 2560
|
||||
},
|
||||
"pronouns": { "type": "string" },
|
||||
"website": { "type": "string", "format": "uri" },
|
||||
"avatar": { "type": "string", "format": "uri" },
|
||||
"banner": { "type": "string", "format": "uri" },
|
||||
"followersCount": { "type": "integer" },
|
||||
@@ -119,6 +131,10 @@
|
||||
"status": {
|
||||
"type": "ref",
|
||||
"ref": "#statusView"
|
||||
},
|
||||
"debug": {
|
||||
"type": "unknown",
|
||||
"description": "Debug information for internal development"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -133,7 +149,8 @@
|
||||
"activitySubscription": {
|
||||
"type": "ref",
|
||||
"ref": "#profileAssociatedActivitySubscription"
|
||||
}
|
||||
},
|
||||
"germ": { "type": "ref", "ref": "#profileAssociatedGerm" }
|
||||
}
|
||||
},
|
||||
"profileAssociatedChat": {
|
||||
@@ -143,6 +160,24 @@
|
||||
"allowIncoming": {
|
||||
"type": "string",
|
||||
"knownValues": ["all", "none", "following"]
|
||||
},
|
||||
"allowGroupInvites": {
|
||||
"type": "string",
|
||||
"knownValues": ["all", "none", "following"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"profileAssociatedGerm": {
|
||||
"type": "object",
|
||||
"required": ["showButtonTo", "messageMeUrl"],
|
||||
"properties": {
|
||||
"messageMeUrl": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"showButtonTo": {
|
||||
"type": "string",
|
||||
"knownValues": ["usersIFollow", "everyone"]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -260,6 +295,7 @@
|
||||
"#savedFeedsPref",
|
||||
"#savedFeedsPrefV2",
|
||||
"#personalDetailsPref",
|
||||
"#declaredAgePref",
|
||||
"#feedViewPref",
|
||||
"#threadViewPref",
|
||||
"#interestsPref",
|
||||
@@ -268,7 +304,8 @@
|
||||
"#bskyAppStatePref",
|
||||
"#labelersPref",
|
||||
"#postInteractionSettingsPref",
|
||||
"#verificationPrefs"
|
||||
"#verificationPrefs",
|
||||
"#liveEventPreferences"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -360,6 +397,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"declaredAgePref": {
|
||||
"type": "object",
|
||||
"description": "Read-only preference containing value(s) inferred from the user's declared birthdate. Absence of this preference object in the response indicates that the user has not made a declaration.",
|
||||
"properties": {
|
||||
"isOverAge13": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates if the user has declared that they are over 13 years of age."
|
||||
},
|
||||
"isOverAge16": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates if the user has declared that they are over 16 years of age."
|
||||
},
|
||||
"isOverAge18": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates if the user has declared that they are over 18 years of age."
|
||||
}
|
||||
}
|
||||
},
|
||||
"feedViewPref": {
|
||||
"type": "object",
|
||||
"required": ["feed"],
|
||||
@@ -398,10 +453,6 @@
|
||||
"type": "string",
|
||||
"description": "Sorting mode for threads.",
|
||||
"knownValues": ["oldest", "newest", "most-likes", "random", "hotness"]
|
||||
},
|
||||
"prioritizeFollowedUsers": {
|
||||
"type": "boolean",
|
||||
"description": "Show followed users at the top of all replies."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -575,6 +626,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"liveEventPreferences": {
|
||||
"type": "object",
|
||||
"description": "Preferences for live events.",
|
||||
"properties": {
|
||||
"hiddenFeedIds": {
|
||||
"description": "A list of feed IDs that the user has hidden from live events.",
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"hideAllFeeds": {
|
||||
"description": "Whether to hide all feeds from live events.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"postInteractionSettingsPref": {
|
||||
"type": "object",
|
||||
"description": "Default post interaction settings for the account. These values should be applied as default values when creating new posts. These refs should mirror the threadgate and postgate records exactly.",
|
||||
@@ -609,6 +676,8 @@
|
||||
"type": "object",
|
||||
"required": ["status", "record"],
|
||||
"properties": {
|
||||
"uri": { "type": "string", "format": "at-uri" },
|
||||
"cid": { "type": "string", "format": "cid" },
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "The status for the account.",
|
||||
@@ -620,6 +689,10 @@
|
||||
"description": "An optional embed associated with the status.",
|
||||
"refs": ["app.bsky.embed.external#view"]
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
|
||||
},
|
||||
"expiresAt": {
|
||||
"type": "string",
|
||||
"description": "The date when this status will expire. The application might choose to no longer return the status after expiration.",
|
||||
@@ -628,6 +701,10 @@
|
||||
"isActive": {
|
||||
"type": "boolean",
|
||||
"description": "True if the status is not expired, false if it is expired. Only present if expiration was set."
|
||||
},
|
||||
"isDisabled": {
|
||||
"type": "boolean",
|
||||
"description": "True if the user's go-live access has been disabled by a moderator, false otherwise."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
},
|
||||
"recId": {
|
||||
"type": "integer",
|
||||
"description": "DEPRECATED: use recIdStr instead."
|
||||
},
|
||||
"recIdStr": {
|
||||
"type": "string",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,13 @@
|
||||
"maxGraphemes": 256,
|
||||
"maxLength": 2560
|
||||
},
|
||||
"pronouns": {
|
||||
"type": "string",
|
||||
"description": "Free-form pronouns text.",
|
||||
"maxGraphemes": 20,
|
||||
"maxLength": 200
|
||||
},
|
||||
"website": { "type": "string", "format": "uri" },
|
||||
"avatar": {
|
||||
"type": "blob",
|
||||
"description": "Small image to be displayed next to posts from account. AKA, 'profile picture'",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.ageassurance.begin",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Initiate Age Assurance for an account.",
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["email", "language", "countryCode"],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "The user's email address to receive Age Assurance instructions."
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "The user's preferred language for communication during the Age Assurance process."
|
||||
},
|
||||
"countryCode": {
|
||||
"type": "string",
|
||||
"description": "An ISO 3166-1 alpha-2 code of the user's location."
|
||||
},
|
||||
"regionCode": {
|
||||
"type": "string",
|
||||
"description": "An optional ISO 3166-2 code of the user's region or state within the country."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#state"
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{ "name": "InvalidEmail" },
|
||||
{ "name": "DidTooLong" },
|
||||
{ "name": "InvalidInitiation" },
|
||||
{ "name": "RegionNotSupported" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.ageassurance.defs",
|
||||
"defs": {
|
||||
"access": {
|
||||
"description": "The access level granted based on Age Assurance data we've processed.",
|
||||
"type": "string",
|
||||
"knownValues": ["unknown", "none", "safe", "full"]
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "The status of the Age Assurance process.",
|
||||
"knownValues": ["unknown", "pending", "assured", "blocked"]
|
||||
},
|
||||
"state": {
|
||||
"type": "object",
|
||||
"description": "The user's computed Age Assurance state.",
|
||||
"required": ["status", "access"],
|
||||
"properties": {
|
||||
"lastInitiatedAt": {
|
||||
"type": "string",
|
||||
"format": "datetime",
|
||||
"description": "The timestamp when this state was last updated."
|
||||
},
|
||||
"status": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#status"
|
||||
},
|
||||
"access": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#access"
|
||||
}
|
||||
}
|
||||
},
|
||||
"stateMetadata": {
|
||||
"type": "object",
|
||||
"description": "Additional metadata needed to compute Age Assurance state client-side.",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"accountCreatedAt": {
|
||||
"type": "string",
|
||||
"format": "datetime",
|
||||
"description": "The account creation timestamp."
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"type": "object",
|
||||
"description": "",
|
||||
"required": ["regions"],
|
||||
"properties": {
|
||||
"regions": {
|
||||
"type": "array",
|
||||
"description": "The per-region Age Assurance configuration.",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#configRegion"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"configRegion": {
|
||||
"type": "object",
|
||||
"description": "The Age Assurance configuration for a specific region.",
|
||||
"required": ["countryCode", "minAccessAge", "rules"],
|
||||
"properties": {
|
||||
"countryCode": {
|
||||
"type": "string",
|
||||
"description": "The ISO 3166-1 alpha-2 country code this configuration applies to."
|
||||
},
|
||||
"regionCode": {
|
||||
"type": "string",
|
||||
"description": "The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country."
|
||||
},
|
||||
"minAccessAge": {
|
||||
"type": "integer",
|
||||
"description": "The minimum age (as a whole integer) required to use Bluesky in this region."
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"description": "The ordered list of Age Assurance rules that apply to this region. Rules should be applied in order, and the first matching rule determines the access level granted. The rules array should always include a default rule as the last item.",
|
||||
"items": {
|
||||
"type": "union",
|
||||
"refs": [
|
||||
"#configRegionRuleDefault",
|
||||
"#configRegionRuleIfDeclaredOverAge",
|
||||
"#configRegionRuleIfDeclaredUnderAge",
|
||||
"#configRegionRuleIfAssuredOverAge",
|
||||
"#configRegionRuleIfAssuredUnderAge",
|
||||
"#configRegionRuleIfAccountNewerThan",
|
||||
"#configRegionRuleIfAccountOlderThan"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"configRegionRuleDefault": {
|
||||
"type": "object",
|
||||
"description": "Age Assurance rule that applies by default.",
|
||||
"required": ["access"],
|
||||
"properties": {
|
||||
"access": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#access"
|
||||
}
|
||||
}
|
||||
},
|
||||
"configRegionRuleIfDeclaredOverAge": {
|
||||
"type": "object",
|
||||
"description": "Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age.",
|
||||
"required": ["age", "access"],
|
||||
"properties": {
|
||||
"age": {
|
||||
"type": "integer",
|
||||
"description": "The age threshold as a whole integer."
|
||||
},
|
||||
"access": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#access"
|
||||
}
|
||||
}
|
||||
},
|
||||
"configRegionRuleIfDeclaredUnderAge": {
|
||||
"type": "object",
|
||||
"description": "Age Assurance rule that applies if the user has declared themselves under a certain age.",
|
||||
"required": ["age", "access"],
|
||||
"properties": {
|
||||
"age": {
|
||||
"type": "integer",
|
||||
"description": "The age threshold as a whole integer."
|
||||
},
|
||||
"access": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#access"
|
||||
}
|
||||
}
|
||||
},
|
||||
"configRegionRuleIfAssuredOverAge": {
|
||||
"type": "object",
|
||||
"description": "Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age.",
|
||||
"required": ["age", "access"],
|
||||
"properties": {
|
||||
"age": {
|
||||
"type": "integer",
|
||||
"description": "The age threshold as a whole integer."
|
||||
},
|
||||
"access": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#access"
|
||||
}
|
||||
}
|
||||
},
|
||||
"configRegionRuleIfAssuredUnderAge": {
|
||||
"type": "object",
|
||||
"description": "Age Assurance rule that applies if the user has been assured to be under a certain age.",
|
||||
"required": ["age", "access"],
|
||||
"properties": {
|
||||
"age": {
|
||||
"type": "integer",
|
||||
"description": "The age threshold as a whole integer."
|
||||
},
|
||||
"access": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#access"
|
||||
}
|
||||
}
|
||||
},
|
||||
"configRegionRuleIfAccountNewerThan": {
|
||||
"type": "object",
|
||||
"description": "Age Assurance rule that applies if the account is equal-to or newer than a certain date.",
|
||||
"required": ["date", "access"],
|
||||
"properties": {
|
||||
"date": {
|
||||
"type": "string",
|
||||
"format": "datetime",
|
||||
"description": "The date threshold as a datetime string."
|
||||
},
|
||||
"access": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#access"
|
||||
}
|
||||
}
|
||||
},
|
||||
"configRegionRuleIfAccountOlderThan": {
|
||||
"type": "object",
|
||||
"description": "Age Assurance rule that applies if the account is older than a certain date.",
|
||||
"required": ["date", "access"],
|
||||
"properties": {
|
||||
"date": {
|
||||
"type": "string",
|
||||
"format": "datetime",
|
||||
"description": "The date threshold as a datetime string."
|
||||
},
|
||||
"access": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#access"
|
||||
}
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"type": "object",
|
||||
"description": "Object used to store Age Assurance data in stash.",
|
||||
"required": ["createdAt", "status", "access", "attemptId", "countryCode"],
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "datetime",
|
||||
"description": "The date and time of this write operation."
|
||||
},
|
||||
"attemptId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for this instance of the Age Assurance flow, in UUID format."
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "The status of the Age Assurance process.",
|
||||
"knownValues": ["unknown", "pending", "assured", "blocked"]
|
||||
},
|
||||
"access": {
|
||||
"description": "The access level granted based on Age Assurance data we've processed.",
|
||||
"type": "string",
|
||||
"knownValues": ["unknown", "none", "safe", "full"]
|
||||
},
|
||||
"countryCode": {
|
||||
"type": "string",
|
||||
"description": "The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow."
|
||||
},
|
||||
"regionCode": {
|
||||
"type": "string",
|
||||
"description": "The ISO 3166-2 region code provided when beginning the Age Assurance flow."
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "The email used for Age Assurance."
|
||||
},
|
||||
"initIp": {
|
||||
"type": "string",
|
||||
"description": "The IP address used when initiating the Age Assurance flow."
|
||||
},
|
||||
"initUa": {
|
||||
"type": "string",
|
||||
"description": "The user agent used when initiating the Age Assurance flow."
|
||||
},
|
||||
"completeIp": {
|
||||
"type": "string",
|
||||
"description": "The IP address used when completing the Age Assurance flow."
|
||||
},
|
||||
"completeUa": {
|
||||
"type": "string",
|
||||
"description": "The user agent used when completing the Age Assurance flow."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.ageassurance.getConfig",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Returns Age Assurance configuration for use on the client.",
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#config"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.ageassurance.getState",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Returns server-computed Age Assurance state, if available, and any additional metadata needed to compute Age Assurance state client-side.",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"required": ["countryCode"],
|
||||
"properties": {
|
||||
"countryCode": { "type": "string" },
|
||||
"regionCode": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["state", "metadata"],
|
||||
"properties": {
|
||||
"state": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#state"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.ageassurance.defs#stateMetadata"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.authCreatePosts",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "permission-set",
|
||||
"title": "Create Bluesky Posts",
|
||||
"title:lang": {},
|
||||
"detail": "Can not update or delete posts.",
|
||||
"detail:lang": {},
|
||||
"permissions": [
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "rpc",
|
||||
"inheritAud": true,
|
||||
"lxm": [
|
||||
"app.bsky.video.uploadVideo",
|
||||
"app.bsky.video.getJobStatus",
|
||||
"app.bsky.video.getUploadLimits"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "repo",
|
||||
"action": ["create"],
|
||||
"collection": [
|
||||
"app.bsky.feed.post",
|
||||
"app.bsky.feed.postgate",
|
||||
"app.bsky.feed.threadgate"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.authDeleteContent",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "permission-set",
|
||||
"title": "Delete Bluesky Content",
|
||||
"title:lang": {},
|
||||
"detail": "Clean up public account history: posts, reposts, and likes.",
|
||||
"detail:lang": {},
|
||||
"permissions": [
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "repo",
|
||||
"action": ["delete"],
|
||||
"collection": [
|
||||
"app.bsky.feed.like",
|
||||
"app.bsky.feed.post",
|
||||
"app.bsky.feed.postgate",
|
||||
"app.bsky.feed.repost",
|
||||
"app.bsky.feed.threadgate"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.authFullApp",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "permission-set",
|
||||
"title": "Full Bluesky Social App Permissions",
|
||||
"title:lang": {},
|
||||
"detail": "Manage all public content and interactions, private preferences and subscriptions, and other Bluesky-specific app features and data.",
|
||||
"detail:lang": {},
|
||||
"permissions": [
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "rpc",
|
||||
"inheritAud": true,
|
||||
"lxm": [
|
||||
"app.bsky.actor.getPreferences",
|
||||
"app.bsky.actor.getProfile",
|
||||
"app.bsky.actor.getProfiles",
|
||||
"app.bsky.actor.getSuggestions",
|
||||
"app.bsky.actor.putPreferences",
|
||||
"app.bsky.actor.searchActors",
|
||||
"app.bsky.actor.searchActorsTypeahead",
|
||||
"app.bsky.bookmark.createBookmark",
|
||||
"app.bsky.bookmark.deleteBookmark",
|
||||
"app.bsky.bookmark.getBookmarks",
|
||||
"app.bsky.contact.dismissMatch",
|
||||
"app.bsky.contact.getMatches",
|
||||
"app.bsky.contact.getSyncStatus",
|
||||
"app.bsky.contact.importContacts",
|
||||
"app.bsky.contact.removeData",
|
||||
"app.bsky.contact.startPhoneVerification",
|
||||
"app.bsky.contact.verifyPhone",
|
||||
"app.bsky.feed.describeFeedGenerator",
|
||||
"app.bsky.feed.getActorFeeds",
|
||||
"app.bsky.feed.getActorLikes",
|
||||
"app.bsky.feed.getAuthorFeed",
|
||||
"app.bsky.feed.getFeed",
|
||||
"app.bsky.feed.getFeedGenerator",
|
||||
"app.bsky.feed.getFeedGenerators",
|
||||
"app.bsky.feed.getFeedSkeleton",
|
||||
"app.bsky.feed.getLikes",
|
||||
"app.bsky.feed.getListFeed",
|
||||
"app.bsky.feed.getPostThread",
|
||||
"app.bsky.feed.getPosts",
|
||||
"app.bsky.feed.getQuotes",
|
||||
"app.bsky.feed.getRepostedBy",
|
||||
"app.bsky.feed.getSuggestedFeeds",
|
||||
"app.bsky.feed.getTimeline",
|
||||
"app.bsky.feed.searchPosts",
|
||||
"app.bsky.feed.sendInteractions",
|
||||
"app.bsky.graph.getActorStarterPacks",
|
||||
"app.bsky.graph.getBlocks",
|
||||
"app.bsky.graph.getFollowers",
|
||||
"app.bsky.graph.getFollows",
|
||||
"app.bsky.graph.getKnownFollowers",
|
||||
"app.bsky.graph.getList",
|
||||
"app.bsky.graph.getListBlocks",
|
||||
"app.bsky.graph.getListMutes",
|
||||
"app.bsky.graph.getLists",
|
||||
"app.bsky.graph.getListsWithMembership",
|
||||
"app.bsky.graph.getMutes",
|
||||
"app.bsky.graph.getRelationships",
|
||||
"app.bsky.graph.getStarterPack",
|
||||
"app.bsky.graph.getStarterPacks",
|
||||
"app.bsky.graph.getStarterPacksWithMembership",
|
||||
"app.bsky.graph.getSuggestedFollowsByActor",
|
||||
"app.bsky.graph.muteActor",
|
||||
"app.bsky.graph.muteActorList",
|
||||
"app.bsky.graph.muteThread",
|
||||
"app.bsky.graph.searchStarterPacks",
|
||||
"app.bsky.graph.unmuteActor",
|
||||
"app.bsky.graph.unmuteActorList",
|
||||
"app.bsky.graph.unmuteThread",
|
||||
"app.bsky.labeler.getServices",
|
||||
"app.bsky.notification.getPreferences",
|
||||
"app.bsky.notification.getUnreadCount",
|
||||
"app.bsky.notification.listActivitySubscriptions",
|
||||
"app.bsky.notification.listNotifications",
|
||||
"app.bsky.notification.putActivitySubscription",
|
||||
"app.bsky.notification.putPreferences",
|
||||
"app.bsky.notification.putPreferencesV2",
|
||||
"app.bsky.notification.registerPush",
|
||||
"app.bsky.notification.unregisterPush",
|
||||
"app.bsky.notification.updateSeen",
|
||||
"app.bsky.unspecced.getAgeAssuranceState",
|
||||
"app.bsky.unspecced.getConfig",
|
||||
"app.bsky.unspecced.getOnboardingSuggestedStarterPacks",
|
||||
"app.bsky.unspecced.getPopularFeedGenerators",
|
||||
"app.bsky.unspecced.getPostThreadOtherV2",
|
||||
"app.bsky.unspecced.getPostThreadV2",
|
||||
"app.bsky.unspecced.getSuggestedFeeds",
|
||||
"app.bsky.unspecced.getSuggestedFeedsSkeleton",
|
||||
"app.bsky.unspecced.getSuggestedStarterPacks",
|
||||
"app.bsky.unspecced.getSuggestedStarterPacksSkeleton",
|
||||
"app.bsky.unspecced.getSuggestedUsers",
|
||||
"app.bsky.unspecced.getSuggestedUsersSkeleton",
|
||||
"app.bsky.unspecced.getSuggestionsSkeleton",
|
||||
"app.bsky.unspecced.getTaggedSuggestions",
|
||||
"app.bsky.unspecced.getTrendingTopics",
|
||||
"app.bsky.unspecced.getTrends",
|
||||
"app.bsky.unspecced.getTrendsSkeleton",
|
||||
"app.bsky.unspecced.initAgeAssurance",
|
||||
"app.bsky.unspecced.searchActorsSkeleton",
|
||||
"app.bsky.unspecced.searchPostsSkeleton",
|
||||
"app.bsky.unspecced.searchStarterPacksSkeleton",
|
||||
"app.bsky.video.getJobStatus",
|
||||
"app.bsky.video.getUploadLimits",
|
||||
"app.bsky.video.uploadVideo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "repo",
|
||||
"action": ["create", "update", "delete"],
|
||||
"collection": [
|
||||
"app.bsky.actor.profile",
|
||||
"app.bsky.actor.status",
|
||||
"app.bsky.feed.like",
|
||||
"app.bsky.feed.post",
|
||||
"app.bsky.feed.postgate",
|
||||
"app.bsky.feed.repost",
|
||||
"app.bsky.feed.threadgate",
|
||||
"app.bsky.graph.block",
|
||||
"app.bsky.graph.follow",
|
||||
"app.bsky.graph.list",
|
||||
"app.bsky.graph.listblock",
|
||||
"app.bsky.graph.listitem",
|
||||
"app.bsky.graph.starterpack",
|
||||
"app.bsky.notification.declaration"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.authManageFeedDeclarations",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "permission-set",
|
||||
"title": "Manage Hosted Feeds",
|
||||
"title:lang": {},
|
||||
"detail": "Configure feed generator declaration records.",
|
||||
"detail:lang": {},
|
||||
"permissions": [
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "repo",
|
||||
"action": ["create", "update", "delete"],
|
||||
"collection": ["app.bsky.feed.generator"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.authManageLabelerService",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "permission-set",
|
||||
"title": "Manage Hosted Labeling Service",
|
||||
"title:lang": {},
|
||||
"detail": "Configure labeler declaration records.",
|
||||
"detail:lang": {},
|
||||
"permissions": [
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "repo",
|
||||
"action": ["create", "update", "delete"],
|
||||
"collection": ["app.bsky.labeler.service"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.authManageModeration",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "permission-set",
|
||||
"title": "Manage Personal Moderation",
|
||||
"title:lang": {},
|
||||
"detail": "Control over blocks, mutes, mod lists, mod services, and preferences.",
|
||||
"detail:lang": {},
|
||||
"permissions": [
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "rpc",
|
||||
"inheritAud": true,
|
||||
"lxm": [
|
||||
"app.bsky.actor.getPreferences",
|
||||
"app.bsky.actor.putPreferences",
|
||||
"app.bsky.graph.muteActor",
|
||||
"app.bsky.graph.muteActorList",
|
||||
"app.bsky.graph.muteThread",
|
||||
"app.bsky.graph.unmuteActor",
|
||||
"app.bsky.graph.unmuteActorList",
|
||||
"app.bsky.graph.unmuteThread"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "repo",
|
||||
"action": ["create", "update", "delete"],
|
||||
"collection": ["app.bsky.graph.block", "app.bsky.graph.listblock"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.authManageNotifications",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "permission-set",
|
||||
"title": "Manage Bluesky Notifications",
|
||||
"title:lang": {},
|
||||
"detail": "View and configure notifications for the Bluesky app.",
|
||||
"detail:lang": {},
|
||||
"permissions": [
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "rpc",
|
||||
"inheritAud": true,
|
||||
"lxm": [
|
||||
"app.bsky.notification.getPreferences",
|
||||
"app.bsky.notification.getUnreadCount",
|
||||
"app.bsky.notification.listActivitySubscriptions",
|
||||
"app.bsky.notification.listNotifications",
|
||||
"app.bsky.notification.putActivitySubscription",
|
||||
"app.bsky.notification.putPreferences",
|
||||
"app.bsky.notification.putPreferencesV2",
|
||||
"app.bsky.notification.registerPush",
|
||||
"app.bsky.notification.unregisterPush",
|
||||
"app.bsky.notification.updateSeen"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.authManageProfile",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "permission-set",
|
||||
"title": "Manage Bluesky Profile",
|
||||
"title:lang": {},
|
||||
"detail": "Update profile data, as well as status and public chat visibility.",
|
||||
"detail:lang": {},
|
||||
"permissions": [
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "repo",
|
||||
"action": ["create", "update", "delete"],
|
||||
"collection": [
|
||||
"app.bsky.actor.profile",
|
||||
"app.bsky.actor.status",
|
||||
"app.bsky.notification.declaration"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.authViewAll",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "permission-set",
|
||||
"title": "Read-only access to all content",
|
||||
"title:lang": {},
|
||||
"detail": "View Bluesky network content from account perspective, and read all notifications and preferences.",
|
||||
"detail:lang": {},
|
||||
"permissions": [
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "rpc",
|
||||
"inheritAud": true,
|
||||
"lxm": [
|
||||
"app.bsky.actor.getPreferences",
|
||||
"app.bsky.actor.getProfile",
|
||||
"app.bsky.actor.getProfiles",
|
||||
"app.bsky.actor.getSuggestions",
|
||||
"app.bsky.actor.searchActors",
|
||||
"app.bsky.actor.searchActorsTypeahead",
|
||||
"app.bsky.bookmark.getBookmarks",
|
||||
"app.bsky.feed.describeFeedGenerator",
|
||||
"app.bsky.feed.getActorFeeds",
|
||||
"app.bsky.feed.getActorLikes",
|
||||
"app.bsky.feed.getAuthorFeed",
|
||||
"app.bsky.feed.getFeed",
|
||||
"app.bsky.feed.getFeedGenerator",
|
||||
"app.bsky.feed.getFeedGenerators",
|
||||
"app.bsky.feed.getFeedSkeleton",
|
||||
"app.bsky.feed.getLikes",
|
||||
"app.bsky.feed.getListFeed",
|
||||
"app.bsky.feed.getPostThread",
|
||||
"app.bsky.feed.getPosts",
|
||||
"app.bsky.feed.getQuotes",
|
||||
"app.bsky.feed.getRepostedBy",
|
||||
"app.bsky.feed.getSuggestedFeeds",
|
||||
"app.bsky.feed.getTimeline",
|
||||
"app.bsky.feed.searchPosts",
|
||||
"app.bsky.graph.getActorStarterPacks",
|
||||
"app.bsky.graph.getBlocks",
|
||||
"app.bsky.graph.getFollowers",
|
||||
"app.bsky.graph.getFollows",
|
||||
"app.bsky.graph.getKnownFollowers",
|
||||
"app.bsky.graph.getListBlocks",
|
||||
"app.bsky.graph.getListMutes",
|
||||
"app.bsky.graph.getLists",
|
||||
"app.bsky.graph.getListsWithMembership",
|
||||
"app.bsky.graph.getMutes",
|
||||
"app.bsky.graph.getRelationships",
|
||||
"app.bsky.graph.getStarterPack",
|
||||
"app.bsky.graph.getStarterPacks",
|
||||
"app.bsky.graph.getStarterPacksWithMembership",
|
||||
"app.bsky.graph.getSuggestedFollowsByActor",
|
||||
"app.bsky.graph.searchStarterPacks",
|
||||
"app.bsky.labeler.getServices",
|
||||
"app.bsky.notification.getPreferences",
|
||||
"app.bsky.notification.getUnreadCount",
|
||||
"app.bsky.notification.listActivitySubscriptions",
|
||||
"app.bsky.notification.listNotifications",
|
||||
"app.bsky.notification.updateSeen",
|
||||
"app.bsky.unspecced.getAgeAssuranceState",
|
||||
"app.bsky.unspecced.getConfig",
|
||||
"app.bsky.unspecced.getOnboardingSuggestedStarterPacks",
|
||||
"app.bsky.unspecced.getPopularFeedGenerators",
|
||||
"app.bsky.unspecced.getPostThreadOtherV2",
|
||||
"app.bsky.unspecced.getPostThreadV2",
|
||||
"app.bsky.unspecced.getSuggestedFeeds",
|
||||
"app.bsky.unspecced.getSuggestedFeedsSkeleton",
|
||||
"app.bsky.unspecced.getSuggestedStarterPacks",
|
||||
"app.bsky.unspecced.getSuggestedStarterPacksSkeleton",
|
||||
"app.bsky.unspecced.getSuggestedUsers",
|
||||
"app.bsky.unspecced.getSuggestedUsersSkeleton",
|
||||
"app.bsky.unspecced.getSuggestionsSkeleton",
|
||||
"app.bsky.unspecced.getTaggedSuggestions",
|
||||
"app.bsky.unspecced.getTrendingTopics",
|
||||
"app.bsky.unspecced.getTrends",
|
||||
"app.bsky.unspecced.getTrendsSkeleton",
|
||||
"app.bsky.unspecced.searchActorsSkeleton",
|
||||
"app.bsky.unspecced.searchPostsSkeleton",
|
||||
"app.bsky.unspecced.searchStarterPacksSkeleton",
|
||||
"app.bsky.video.getUploadLimits"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.contact.defs",
|
||||
"defs": {
|
||||
"matchAndContactIndex": {
|
||||
"description": "Associates a profile with the positional index of the contact import input in the call to `app.bsky.contact.importContacts`, so clients can know which phone caused a particular match.",
|
||||
"type": "object",
|
||||
"required": ["match", "contactIndex"],
|
||||
"properties": {
|
||||
"match": {
|
||||
"description": "Profile of the matched user.",
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.actor.defs#profileView"
|
||||
},
|
||||
"contactIndex": {
|
||||
"description": "The index of this match in the import contact input.",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 999
|
||||
}
|
||||
}
|
||||
},
|
||||
"syncStatus": {
|
||||
"type": "object",
|
||||
"required": ["syncedAt", "matchesCount"],
|
||||
"properties": {
|
||||
"syncedAt": {
|
||||
"description": "Last date when contacts where imported.",
|
||||
"type": "string",
|
||||
"format": "datetime"
|
||||
},
|
||||
"matchesCount": {
|
||||
"description": "Number of existing contact matches resulting of the user imports and of their imported contacts having imported the user. Matches stop being counted when the user either follows the matched contact or dismisses the match.",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
"description": "A stash object to be sent via bsync representing a notification to be created.",
|
||||
"type": "object",
|
||||
"required": ["from", "to"],
|
||||
"properties": {
|
||||
"from": {
|
||||
"description": "The DID of who this notification comes from.",
|
||||
"type": "string",
|
||||
"format": "did"
|
||||
},
|
||||
"to": {
|
||||
"description": "The DID of who this notification should go to.",
|
||||
"type": "string",
|
||||
"format": "did"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.contact.dismissMatch",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Removes a match that was found via contact import. It shouldn't appear again if the same contact is re-imported. Requires authentication.",
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["subject"],
|
||||
"properties": {
|
||||
"subject": {
|
||||
"description": "The subject's DID to dismiss the match with.",
|
||||
"type": "string",
|
||||
"format": "did"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"name": "InvalidDid"
|
||||
},
|
||||
{
|
||||
"name": "InternalError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.contact.getMatches",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Returns the matched contacts (contacts that were mutually imported). Excludes dismissed matches. Requires authentication.",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": 50
|
||||
},
|
||||
"cursor": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["matches"],
|
||||
"properties": {
|
||||
"cursor": { "type": "string" },
|
||||
"matches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.actor.defs#profileView"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"name": "InvalidDid"
|
||||
},
|
||||
{
|
||||
"name": "InvalidLimit"
|
||||
},
|
||||
{
|
||||
"name": "InvalidCursor"
|
||||
},
|
||||
{
|
||||
"name": "InternalError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.contact.getSyncStatus",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Gets the user's current contact import status. Requires authentication.",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"syncStatus": {
|
||||
"description": "If present, indicates the user has imported their contacts. If not present, indicates the user never used the feature or called `app.bsky.contact.removeData` and didn't import again since.",
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.contact.defs#syncStatus"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"name": "InvalidDid"
|
||||
},
|
||||
{
|
||||
"name": "InternalError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.contact.importContacts",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Import contacts for securely matching with other users. This follows the protocol explained in https://docs.bsky.app/blog/contact-import-rfc. Requires authentication.",
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["token", "contacts"],
|
||||
"properties": {
|
||||
"token": {
|
||||
"description": "JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`.",
|
||||
"type": "string"
|
||||
},
|
||||
"contacts": {
|
||||
"description": "List of phone numbers in global E.164 format (e.g., '+12125550123'). Phone numbers that cannot be normalized into a valid phone number will be discarded. Should not repeat the 'phone' input used in `app.bsky.contact.verifyPhone`.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minLength": 1,
|
||||
"maxLength": 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["matchesAndContactIndexes"],
|
||||
"properties": {
|
||||
"matchesAndContactIndexes": {
|
||||
"description": "The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.contact.defs#matchAndContactIndex"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"name": "InvalidDid"
|
||||
},
|
||||
{
|
||||
"name": "InvalidContacts"
|
||||
},
|
||||
{
|
||||
"name": "TooManyContacts"
|
||||
},
|
||||
{
|
||||
"name": "InvalidToken"
|
||||
},
|
||||
{
|
||||
"name": "InternalError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.contact.removeData",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Removes all stored hashes used for contact matching, existing matches, and sync status. Requires authentication.",
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"name": "InvalidDid"
|
||||
},
|
||||
{
|
||||
"name": "InternalError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.contact.sendNotification",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "System endpoint to send notifications related to contact imports. Requires role authentication.",
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["from", "to"],
|
||||
"properties": {
|
||||
"from": {
|
||||
"description": "The DID of who this notification comes from.",
|
||||
"type": "string",
|
||||
"format": "did"
|
||||
},
|
||||
"to": {
|
||||
"description": "The DID of who this notification should go to.",
|
||||
"type": "string",
|
||||
"format": "did"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.contact.startPhoneVerification",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Starts a phone verification flow. The phone passed will receive a code via SMS that should be passed to `app.bsky.contact.verifyPhone`. Requires authentication.",
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["phone"],
|
||||
"properties": {
|
||||
"phone": {
|
||||
"description": "The phone number to receive the code via SMS.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"name": "RateLimitExceeded"
|
||||
},
|
||||
{
|
||||
"name": "InvalidDid"
|
||||
},
|
||||
{
|
||||
"name": "InvalidPhone"
|
||||
},
|
||||
{
|
||||
"name": "InternalError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.contact.verifyPhone",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Verifies control over a phone number with a code received via SMS and starts a contact import session. Requires authentication.",
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["phone", "code"],
|
||||
"properties": {
|
||||
"phone": {
|
||||
"description": "The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`.",
|
||||
"type": "string"
|
||||
},
|
||||
"code": {
|
||||
"description": "The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["token"],
|
||||
"properties": {
|
||||
"token": {
|
||||
"description": "JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"name": "RateLimitExceeded"
|
||||
},
|
||||
{
|
||||
"name": "InvalidDid"
|
||||
},
|
||||
{
|
||||
"name": "InvalidPhone"
|
||||
},
|
||||
{
|
||||
"name": "InvalidCode"
|
||||
},
|
||||
{
|
||||
"name": "InternalError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.draft.createDraft",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Inserts a draft using private storage (stash). An upper limit of drafts might be enforced. Requires authentication.",
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["draft"],
|
||||
"properties": {
|
||||
"draft": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.draft.defs#draft"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The ID of the created draft."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"name": "DraftLimitReached",
|
||||
"description": "Trying to insert a new draft when the limit was already reached."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.draft.defs",
|
||||
"defs": {
|
||||
"draftWithId": {
|
||||
"description": "A draft with an identifier, used to store drafts in private storage (stash).",
|
||||
"type": "object",
|
||||
"required": ["id", "draft"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "A TID to be used as a draft identifier.",
|
||||
"type": "string",
|
||||
"format": "tid"
|
||||
},
|
||||
"draft": {
|
||||
"type": "ref",
|
||||
"ref": "#draft"
|
||||
}
|
||||
}
|
||||
},
|
||||
"draft": {
|
||||
"description": "A draft containing an array of draft posts.",
|
||||
"type": "object",
|
||||
"required": ["posts"],
|
||||
"properties": {
|
||||
"deviceId": {
|
||||
"type": "string",
|
||||
"description": "UUIDv4 identifier of the device that created this draft.",
|
||||
"maxLength": 100
|
||||
},
|
||||
"deviceName": {
|
||||
"type": "string",
|
||||
"description": "The device and/or platform on which the draft was created.",
|
||||
"maxLength": 100
|
||||
},
|
||||
"posts": {
|
||||
"description": "Array of draft posts that compose this draft.",
|
||||
"type": "array",
|
||||
"minLength": 1,
|
||||
"maxLength": 100,
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "#draftPost"
|
||||
}
|
||||
},
|
||||
"langs": {
|
||||
"type": "array",
|
||||
"description": "Indicates human language of posts primary text content.",
|
||||
"maxLength": 3,
|
||||
"items": { "type": "string", "format": "language" }
|
||||
},
|
||||
"postgateEmbeddingRules": {
|
||||
"description": "Embedding rules for the postgates to be created when this draft is published.",
|
||||
"type": "array",
|
||||
"maxLength": 5,
|
||||
"items": {
|
||||
"type": "union",
|
||||
"refs": ["app.bsky.feed.postgate#disableRule"]
|
||||
}
|
||||
},
|
||||
"threadgateAllow": {
|
||||
"description": "Allow-rules for the threadgate to be created when this draft is published.",
|
||||
"type": "array",
|
||||
"maxLength": 5,
|
||||
"items": {
|
||||
"type": "union",
|
||||
"refs": [
|
||||
"app.bsky.feed.threadgate#mentionRule",
|
||||
"app.bsky.feed.threadgate#followerRule",
|
||||
"app.bsky.feed.threadgate#followingRule",
|
||||
"app.bsky.feed.threadgate#listRule"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"draftPost": {
|
||||
"description": "One of the posts that compose a draft.",
|
||||
"type": "object",
|
||||
"required": ["text"],
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"maxLength": 10000,
|
||||
"maxGraphemes": 1000,
|
||||
"description": "The primary post content. It has a higher limit than post contents to allow storing a larger text that can later be refined into smaller posts."
|
||||
},
|
||||
"labels": {
|
||||
"type": "union",
|
||||
"description": "Self-label values for this post. Effectively content warnings.",
|
||||
"refs": ["com.atproto.label.defs#selfLabels"]
|
||||
},
|
||||
"embedImages": {
|
||||
"type": "array",
|
||||
"items": { "type": "ref", "ref": "#draftEmbedImage" },
|
||||
"maxLength": 4
|
||||
},
|
||||
"embedVideos": {
|
||||
"type": "array",
|
||||
"items": { "type": "ref", "ref": "#draftEmbedVideo" },
|
||||
"maxLength": 1
|
||||
},
|
||||
"embedExternals": {
|
||||
"type": "array",
|
||||
"items": { "type": "ref", "ref": "#draftEmbedExternal" },
|
||||
"maxLength": 1
|
||||
},
|
||||
"embedRecords": {
|
||||
"type": "array",
|
||||
"items": { "type": "ref", "ref": "#draftEmbedRecord" },
|
||||
"maxLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"draftView": {
|
||||
"description": "View to present drafts data to users.",
|
||||
"type": "object",
|
||||
"required": ["id", "draft", "createdAt", "updatedAt"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "A TID to be used as a draft identifier.",
|
||||
"type": "string",
|
||||
"format": "tid"
|
||||
},
|
||||
"draft": {
|
||||
"type": "ref",
|
||||
"ref": "#draft"
|
||||
},
|
||||
"createdAt": {
|
||||
"description": "The time the draft was created.",
|
||||
"type": "string",
|
||||
"format": "datetime"
|
||||
},
|
||||
"updatedAt": {
|
||||
"description": "The time the draft was last updated.",
|
||||
"type": "string",
|
||||
"format": "datetime"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"draftEmbedLocalRef": {
|
||||
"type": "object",
|
||||
"required": ["path"],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Local, on-device ref to file to be embedded. Embeds are currently device-bound for drafts.",
|
||||
"minLength": 1,
|
||||
"maxLength": 1024
|
||||
}
|
||||
}
|
||||
},
|
||||
"draftEmbedCaption": {
|
||||
"type": "object",
|
||||
"required": ["lang", "content"],
|
||||
"properties": {
|
||||
"lang": {
|
||||
"type": "string",
|
||||
"format": "language"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"maxLength": 10000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"draftEmbedImage": {
|
||||
"type": "object",
|
||||
"required": ["localRef"],
|
||||
"properties": {
|
||||
"localRef": {
|
||||
"type": "ref",
|
||||
"ref": "#draftEmbedLocalRef"
|
||||
},
|
||||
"alt": {
|
||||
"type": "string",
|
||||
"maxGraphemes": 2000
|
||||
}
|
||||
}
|
||||
},
|
||||
"draftEmbedVideo": {
|
||||
"type": "object",
|
||||
"required": ["localRef"],
|
||||
"properties": {
|
||||
"localRef": {
|
||||
"type": "ref",
|
||||
"ref": "#draftEmbedLocalRef"
|
||||
},
|
||||
"alt": {
|
||||
"type": "string",
|
||||
"maxGraphemes": 2000
|
||||
},
|
||||
"captions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "#draftEmbedCaption"
|
||||
},
|
||||
"maxLength": 20
|
||||
}
|
||||
}
|
||||
},
|
||||
"draftEmbedExternal": {
|
||||
"type": "object",
|
||||
"required": ["uri"],
|
||||
"properties": {
|
||||
"uri": { "type": "string", "format": "uri" }
|
||||
}
|
||||
},
|
||||
"draftEmbedRecord": {
|
||||
"type": "object",
|
||||
"required": ["record"],
|
||||
"properties": {
|
||||
"record": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.draft.deleteDraft",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Deletes a draft by ID. Requires authentication.",
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "tid"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.draft.getDrafts",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Gets views of user drafts. Requires authentication.",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": 50
|
||||
},
|
||||
"cursor": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["drafts"],
|
||||
"properties": {
|
||||
"cursor": { "type": "string" },
|
||||
"drafts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.draft.defs#draftView"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.draft.updateDraft",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Updates a draft using private storage (stash). If the draft ID points to a non-existing ID, the update will be silently ignored. This is done because updates don't enforce draft limit, so it accepts all writes, but will ignore invalid ones. Requires authentication.",
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["draft"],
|
||||
"properties": {
|
||||
"draft": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.draft.defs#draftWithId"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,9 @@
|
||||
"properties": {
|
||||
"image": {
|
||||
"type": "blob",
|
||||
"description": "The raw image file. May be up to 2 MB, formerly limited to 1 MB.",
|
||||
"accept": ["image/*"],
|
||||
"maxSize": 1000000
|
||||
"maxSize": 2000000
|
||||
},
|
||||
"alt": {
|
||||
"type": "string",
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
"aspectRatio": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.embed.defs#aspectRatio"
|
||||
},
|
||||
"presentation": {
|
||||
"type": "string",
|
||||
"description": "A hint to the client about how to present the video.",
|
||||
"knownValues": ["default", "gif"]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -60,6 +65,11 @@
|
||||
"aspectRatio": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.embed.defs#aspectRatio"
|
||||
},
|
||||
"presentation": {
|
||||
"type": "string",
|
||||
"description": "A hint to the client about how to present the video.",
|
||||
"knownValues": ["default", "gif"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,11 @@
|
||||
"type": "array",
|
||||
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
|
||||
},
|
||||
"threadgate": { "type": "ref", "ref": "#threadgateView" }
|
||||
"threadgate": { "type": "ref", "ref": "#threadgateView" },
|
||||
"debug": {
|
||||
"type": "unknown",
|
||||
"description": "Debug information for internal development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewerState": {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"type": "object",
|
||||
"required": ["interactions"],
|
||||
"properties": {
|
||||
"feed": { "type": "string", "format": "at-uri" },
|
||||
"interactions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"createdAt": { "type": "string", "format": "datetime" },
|
||||
"hiddenReplies": {
|
||||
"type": "array",
|
||||
"maxLength": 50,
|
||||
"maxLength": 300,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "at-uri"
|
||||
|
||||
@@ -159,6 +159,26 @@
|
||||
"type": "string",
|
||||
"format": "at-uri",
|
||||
"description": "if the actor is followed by this DID, contains the AT-URI of the follow record"
|
||||
},
|
||||
"blocking": {
|
||||
"type": "string",
|
||||
"format": "at-uri",
|
||||
"description": "if the actor blocks this DID, this is the AT-URI of the block record"
|
||||
},
|
||||
"blockedBy": {
|
||||
"type": "string",
|
||||
"format": "at-uri",
|
||||
"description": "if the actor is blocked by this DID, contains the AT-URI of the block record"
|
||||
},
|
||||
"blockingByList": {
|
||||
"type": "string",
|
||||
"format": "at-uri",
|
||||
"description": "if the actor blocks this DID via a block list, this is the AT-URI of the listblock record"
|
||||
},
|
||||
"blockedByList": {
|
||||
"type": "string",
|
||||
"format": "at-uri",
|
||||
"description": "if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"required": ["subject", "createdAt"],
|
||||
"properties": {
|
||||
"subject": { "type": "string", "format": "did" },
|
||||
"createdAt": { "type": "string", "format": "datetime" }
|
||||
"createdAt": { "type": "string", "format": "datetime" },
|
||||
"via": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,14 +25,18 @@
|
||||
"ref": "app.bsky.actor.defs#profileView"
|
||||
}
|
||||
},
|
||||
"recIdStr": {
|
||||
"type": "string",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
},
|
||||
"isFallback": {
|
||||
"type": "boolean",
|
||||
"description": "If true, response has fallen-back to generic results, and is not scoped using relativeToDid",
|
||||
"description": "DEPRECATED, unused. Previously: if true, response has fallen-back to generic results, and is not scoped using relativeToDid",
|
||||
"default": false
|
||||
},
|
||||
"recId": {
|
||||
"type": "integer",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
"description": "DEPRECATED: use recIdStr instead."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,8 @@
|
||||
"unverified",
|
||||
"like-via-repost",
|
||||
"repost-via-repost",
|
||||
"subscribed-post"
|
||||
"subscribed-post",
|
||||
"contact-match"
|
||||
]
|
||||
},
|
||||
"reasonSubject": { "type": "string", "format": "at-uri" },
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.unspecced.getOnboardingSuggestedStarterPacks",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Get a list of suggested starterpacks for onboarding",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 25,
|
||||
"default": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["starterPacks"],
|
||||
"properties": {
|
||||
"starterPacks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.graph.defs#starterPackView"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Get a skeleton of suggested starterpacks for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getOnboardingSuggestedStarterPacks",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
"viewer": {
|
||||
"type": "string",
|
||||
"format": "did",
|
||||
"description": "DID of the account making the request (not included for public/unauthenticated queries)."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 25,
|
||||
"default": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["starterPacks"],
|
||||
"properties": {
|
||||
"starterPacks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "at-uri"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Get a skeleton of suggested users for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedOnboardingUsers",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
"viewer": {
|
||||
"type": "string",
|
||||
"format": "did",
|
||||
"description": "DID of the account making the request (not included for public/unauthenticated queries)."
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Category of users to get suggestions for."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 50,
|
||||
"default": 25
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["dids"],
|
||||
"properties": {
|
||||
"dids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "did"
|
||||
}
|
||||
},
|
||||
"recId": {
|
||||
"type": "string",
|
||||
"description": "DEPRECATED: use recIdStr instead."
|
||||
},
|
||||
"recIdStr": {
|
||||
"type": "string",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,6 @@
|
||||
"type": "string",
|
||||
"format": "at-uri",
|
||||
"description": "Reference (AT-URI) to post record. This is the anchor post."
|
||||
},
|
||||
"prioritizeFollowedUsers": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to prioritize posts from followed users. It only has effect when the user is authenticated.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -33,11 +33,6 @@
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
},
|
||||
"prioritizeFollowedUsers": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to prioritize posts from followed users. It only has effect when the user is authenticated.",
|
||||
"default": false
|
||||
},
|
||||
"sort": {
|
||||
"type": "string",
|
||||
"description": "Sorting for the thread replies.",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.unspecced.getSuggestedOnboardingUsers",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Get a list of suggested users for onboarding",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Category of users to get suggestions for."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 50,
|
||||
"default": 25
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["actors"],
|
||||
"properties": {
|
||||
"actors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.actor.defs#profileView"
|
||||
}
|
||||
},
|
||||
"recId": {
|
||||
"type": "string",
|
||||
"description": "DEPRECATED: use recIdStr instead."
|
||||
},
|
||||
"recIdStr": {
|
||||
"type": "string",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,14 @@
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.actor.defs#profileView"
|
||||
}
|
||||
},
|
||||
"recId": {
|
||||
"type": "string",
|
||||
"description": "DEPRECATED: use recIdStr instead."
|
||||
},
|
||||
"recIdStr": {
|
||||
"type": "string",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.unspecced.getSuggestedUsersForDiscover",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Get a list of suggested users for the Discover page",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 50,
|
||||
"default": 25
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["actors"],
|
||||
"properties": {
|
||||
"actors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.actor.defs#profileView"
|
||||
}
|
||||
},
|
||||
"recIdStr": {
|
||||
"type": "string",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.unspecced.getSuggestedUsersForDiscoverSkeleton",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Get a skeleton of suggested users for the Discover page. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedUsersForDiscover",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
"viewer": {
|
||||
"type": "string",
|
||||
"format": "did",
|
||||
"description": "DID of the account making the request (not included for public/unauthenticated queries)."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 50,
|
||||
"default": 25
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["dids"],
|
||||
"properties": {
|
||||
"dids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "did"
|
||||
}
|
||||
},
|
||||
"recIdStr": {
|
||||
"type": "string",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.unspecced.getSuggestedUsersForExplore",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Get a list of suggested users for the Explore page",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Category of users to get suggestions for."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 50,
|
||||
"default": 25
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["actors"],
|
||||
"properties": {
|
||||
"actors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.actor.defs#profileView"
|
||||
}
|
||||
},
|
||||
"recIdStr": {
|
||||
"type": "string",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.unspecced.getSuggestedUsersForExploreSkeleton",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Get a skeleton of suggested users for the Explore page. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedUsersForExplore",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
"viewer": {
|
||||
"type": "string",
|
||||
"format": "did",
|
||||
"description": "DID of the account making the request (not included for public/unauthenticated queries)."
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Category of users to get suggestions for."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 50,
|
||||
"default": 25
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["dids"],
|
||||
"properties": {
|
||||
"dids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "did"
|
||||
}
|
||||
},
|
||||
"recIdStr": {
|
||||
"type": "string",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.unspecced.getSuggestedUsersForSeeMore",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Get a list of suggested users for the See More page",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Category of users to get suggestions for."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 50,
|
||||
"default": 25
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["actors"],
|
||||
"properties": {
|
||||
"actors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.actor.defs#profileView"
|
||||
}
|
||||
},
|
||||
"recIdStr": {
|
||||
"type": "string",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.unspecced.getSuggestedUsersForSeeMoreSkeleton",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Get a skeleton of suggested users for the See More page. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedUsersForSeeMore",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
"viewer": {
|
||||
"type": "string",
|
||||
"format": "did",
|
||||
"description": "DID of the account making the request (not included for public/unauthenticated queries)."
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Category of users to get suggestions for."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 50,
|
||||
"default": 25
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["dids"],
|
||||
"properties": {
|
||||
"dids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "did"
|
||||
}
|
||||
},
|
||||
"recIdStr": {
|
||||
"type": "string",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,14 @@
|
||||
"type": "string",
|
||||
"format": "did"
|
||||
}
|
||||
},
|
||||
"recId": {
|
||||
"type": "string",
|
||||
"description": "DEPRECATED: use recIdStr instead."
|
||||
},
|
||||
"recIdStr": {
|
||||
"type": "string",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
},
|
||||
"recId": {
|
||||
"type": "integer",
|
||||
"description": "DEPRECATED: use recIdStr instead."
|
||||
},
|
||||
"recIdStr": {
|
||||
"type": "string",
|
||||
"description": "Snowflake for this recommendation, use when submitting recommendation events."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
"allowIncoming": {
|
||||
"type": "string",
|
||||
"knownValues": ["all", "none", "following"]
|
||||
},
|
||||
"allowGroupInvites": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Declaration about group chat invitation preferences for the record owner.",
|
||||
"type": "string",
|
||||
"knownValues": ["all", "none", "following"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"lexicon": 1,
|
||||
"id": "chat.bsky.actor.defs",
|
||||
"defs": {
|
||||
"memberRole": {
|
||||
"type": "string",
|
||||
"knownValues": ["owner", "standard"]
|
||||
},
|
||||
"profileViewBasic": {
|
||||
"type": "object",
|
||||
"required": ["did", "handle"],
|
||||
@@ -23,6 +27,7 @@
|
||||
"type": "array",
|
||||
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
|
||||
},
|
||||
"createdAt": { "type": "string", "format": "datetime" },
|
||||
"chatDisabled": {
|
||||
"type": "boolean",
|
||||
"description": "Set to true when the actor cannot actively participate in conversations"
|
||||
@@ -30,8 +35,45 @@
|
||||
"verification": {
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.actor.defs#verificationState"
|
||||
},
|
||||
"kind": {
|
||||
"description": "Union field that has data specific to different kinds of convos.",
|
||||
"type": "union",
|
||||
"refs": [
|
||||
"#directConvoMember",
|
||||
"#groupConvoMember",
|
||||
"#pastGroupConvoMember"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"directConvoMember": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here].",
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"groupConvoMember": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. A current group convo member.",
|
||||
"type": "object",
|
||||
"required": ["role"],
|
||||
"properties": {
|
||||
"addedBy": {
|
||||
"description": "Who added this member. Only present if the member was added (instead of joining via link).",
|
||||
"type": "ref",
|
||||
"ref": "#profileViewBasic"
|
||||
},
|
||||
"role": {
|
||||
"description": "The member's role within this conversation. Only present in group conversation member lists.",
|
||||
"type": "ref",
|
||||
"ref": "#memberRole"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pastGroupConvoMember": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. A past group convo member.",
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "chat.bsky.authFullChatClient",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "permission-set",
|
||||
"title": "Full Chat Client (All Conversations)",
|
||||
"title:lang": {},
|
||||
"detail": "Control of all chat conversations and configuration management.",
|
||||
"detail:lang": {
|
||||
"en": "All Chat Conversations"
|
||||
},
|
||||
"permissions": [
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "rpc",
|
||||
"inheritAud": true,
|
||||
"lxm": [
|
||||
"chat.bsky.actor.deleteAccount",
|
||||
"chat.bsky.convo.acceptConvo",
|
||||
"chat.bsky.convo.addReaction",
|
||||
"chat.bsky.convo.deleteMessageForSelf",
|
||||
"chat.bsky.convo.exportAccountData",
|
||||
"chat.bsky.convo.getConvo",
|
||||
"chat.bsky.convo.getConvoAvailability",
|
||||
"chat.bsky.convo.getConvoForMembers",
|
||||
"chat.bsky.convo.getLog",
|
||||
"chat.bsky.convo.getMessages",
|
||||
"chat.bsky.convo.leaveConvo",
|
||||
"chat.bsky.convo.listConvos",
|
||||
"chat.bsky.convo.muteConvo",
|
||||
"chat.bsky.convo.removeReaction",
|
||||
"chat.bsky.convo.sendMessage",
|
||||
"chat.bsky.convo.sendMessageBatch",
|
||||
"chat.bsky.convo.unmuteConvo",
|
||||
"chat.bsky.convo.updateAllRead",
|
||||
"chat.bsky.convo.updateRead"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "permission",
|
||||
"resource": "repo",
|
||||
"action": ["create", "update", "delete"],
|
||||
"collection": ["chat.bsky.actor.declaration"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Marks a conversation as accepted, so it is shown in the list of accepted convos instead on the request convos.",
|
||||
"errors": [{ "name": "InvalidConvo" }],
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{ "name": "InvalidConvo" },
|
||||
{
|
||||
"name": "ReactionNotAllowed",
|
||||
"description": "Indicates that reactions are not allowed on this message, e.g. because it is a system message."
|
||||
},
|
||||
{
|
||||
"name": "ReactionMessageDeleted",
|
||||
"description": "Indicates that the message has been deleted and reactions can no longer be added/removed."
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
"lexicon": 1,
|
||||
"id": "chat.bsky.convo.defs",
|
||||
"defs": {
|
||||
"convoKind": {
|
||||
"type": "string",
|
||||
"knownValues": ["direct", "group"]
|
||||
},
|
||||
"convoLockStatus": {
|
||||
"type": "string",
|
||||
"knownValues": ["unlocked", "locked", "locked-permanently"]
|
||||
},
|
||||
"convoStatus": {
|
||||
"type": "string",
|
||||
"knownValues": ["request", "accepted"]
|
||||
},
|
||||
"messageRef": {
|
||||
"type": "object",
|
||||
"required": ["did", "messageId", "convoId"],
|
||||
@@ -60,6 +72,181 @@
|
||||
"sentAt": { "type": "string", "format": "datetime" }
|
||||
}
|
||||
},
|
||||
"systemMessageReferredUser": {
|
||||
"type": "object",
|
||||
"required": ["did"],
|
||||
"properties": {
|
||||
"did": { "type": "string", "format": "did" }
|
||||
}
|
||||
},
|
||||
"systemMessageView": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here].",
|
||||
"type": "object",
|
||||
"required": ["id", "rev", "sentAt", "data"],
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"rev": { "type": "string" },
|
||||
"sentAt": { "type": "string", "format": "datetime" },
|
||||
"data": {
|
||||
"type": "union",
|
||||
"refs": [
|
||||
"#systemMessageDataAddMember",
|
||||
"#systemMessageDataRemoveMember",
|
||||
"#systemMessageDataMemberJoin",
|
||||
"#systemMessageDataMemberLeave",
|
||||
"#systemMessageDataLockConvo",
|
||||
"#systemMessageDataUnlockConvo",
|
||||
"#systemMessageDataLockConvoPermanently",
|
||||
"#systemMessageDataEditGroup",
|
||||
"#systemMessageDataCreateJoinLink",
|
||||
"#systemMessageDataEditJoinLink",
|
||||
"#systemMessageDataEnableJoinLink",
|
||||
"#systemMessageDataDisableJoinLink"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemMessageDataAddMember": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. System message indicating a user was added to the group convo.",
|
||||
"type": "object",
|
||||
"required": ["member", "role", "addedBy"],
|
||||
"properties": {
|
||||
"member": {
|
||||
"description": "Current view of the member who was added.",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageReferredUser"
|
||||
},
|
||||
"role": {
|
||||
"description": "Role the user was added to the group with. The role from 'member' will reflect the current data, not historical.",
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#memberRole"
|
||||
},
|
||||
"addedBy": {
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageReferredUser"
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemMessageDataRemoveMember": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. System message indicating a user was removed from the group convo.",
|
||||
"type": "object",
|
||||
"required": ["member", "removedBy"],
|
||||
"properties": {
|
||||
"member": {
|
||||
"description": "Current view of the member who was removed.",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageReferredUser"
|
||||
},
|
||||
"removedBy": {
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageReferredUser"
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemMessageDataMemberJoin": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. System message indicating a user joined the group convo via join link.",
|
||||
"type": "object",
|
||||
"required": ["member", "role"],
|
||||
"properties": {
|
||||
"member": {
|
||||
"description": "Current view of the member who joined.",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageReferredUser"
|
||||
},
|
||||
"role": {
|
||||
"description": "Role the user was added to the group with. The role from 'member' will reflect the current data, not historical.",
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#memberRole"
|
||||
},
|
||||
"approvedBy": {
|
||||
"description": "If join link was configured to require approval, this will be set to who approved the request. Undefined if approval was not required.",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageReferredUser"
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemMessageDataMemberLeave": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. System message indicating a user voluntarily left the group convo.",
|
||||
"type": "object",
|
||||
"required": ["member"],
|
||||
"properties": {
|
||||
"member": {
|
||||
"description": "Current view of the member who left the group.",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageReferredUser"
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemMessageDataLockConvo": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. System message indicating the group convo was locked.",
|
||||
"type": "object",
|
||||
"required": ["lockedBy"],
|
||||
"properties": {
|
||||
"lockedBy": {
|
||||
"description": "Current view of the member who locked the group.",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageReferredUser"
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemMessageDataUnlockConvo": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. System message indicating the group convo was unlocked.",
|
||||
"type": "object",
|
||||
"required": ["unlockedBy"],
|
||||
"properties": {
|
||||
"unlockedBy": {
|
||||
"description": "Current view of the member who unlocked the group.",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageReferredUser"
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemMessageDataLockConvoPermanently": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. System message indicating the group convo was locked permanently.",
|
||||
"type": "object",
|
||||
"required": ["lockedBy"],
|
||||
"properties": {
|
||||
"lockedBy": {
|
||||
"description": "Current view of the member who locked the group.",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageReferredUser"
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemMessageDataEditGroup": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. System message indicating the group info was edited.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"oldName": {
|
||||
"description": "Group name that was replaced.",
|
||||
"type": "string"
|
||||
},
|
||||
"newName": {
|
||||
"description": "Group name that replaced the old.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemMessageDataCreateJoinLink": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. System message indicating the group join link was created.",
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"systemMessageDataEditJoinLink": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. System message indicating the group join link was edited.",
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"systemMessageDataEnableJoinLink": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. System message indicating the group join link was enabled.",
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"systemMessageDataDisableJoinLink": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. System message indicating the group join link was disabled.",
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"deletedMessageView": {
|
||||
"type": "object",
|
||||
"required": ["id", "rev", "sender", "sentAt"],
|
||||
@@ -108,6 +295,7 @@
|
||||
"id": { "type": "string" },
|
||||
"rev": { "type": "string" },
|
||||
"members": {
|
||||
"description": "Members of this conversation. For direct convos, it will be an immutable list of the 2 members. For group convos, it will a list of important members (the first few members, the viewer, the member who invited the viewer, the member who sent the last message, the member who sent the last reaction), but will not contain the full list of members. Use chat.bsky.convo.getConvoMembers to list all members.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
@@ -116,7 +304,7 @@
|
||||
},
|
||||
"lastMessage": {
|
||||
"type": "union",
|
||||
"refs": ["#messageView", "#deletedMessageView"]
|
||||
"refs": ["#messageView", "#deletedMessageView", "#systemMessageView"]
|
||||
},
|
||||
"lastReaction": {
|
||||
"type": "union",
|
||||
@@ -124,13 +312,51 @@
|
||||
},
|
||||
"muted": { "type": "boolean" },
|
||||
"status": {
|
||||
"type": "string",
|
||||
"knownValues": ["request", "accepted"]
|
||||
"description": "Convo status for the viewer member (not the convo itself).",
|
||||
"type": "ref",
|
||||
"ref": "#convoStatus"
|
||||
},
|
||||
"unreadCount": { "type": "integer" }
|
||||
"unreadCount": { "type": "integer" },
|
||||
"kind": {
|
||||
"description": "Union field that has data specific to different kinds of convos.",
|
||||
"type": "union",
|
||||
"refs": ["#directConvo", "#groupConvo"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"directConvo": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here].",
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"groupConvo": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here].",
|
||||
"type": "object",
|
||||
"required": ["name", "lockStatus", "memberCount"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The display name of the group conversation.",
|
||||
"maxGraphemes": 128,
|
||||
"maxLength": 1280
|
||||
},
|
||||
"memberCount": {
|
||||
"type": "integer",
|
||||
"description": "The total number of members in the group conversation."
|
||||
},
|
||||
"joinLink": {
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.group.defs#joinLinkView"
|
||||
},
|
||||
"lockStatus": {
|
||||
"description": "The lock status of the conversation.",
|
||||
"type": "ref",
|
||||
"ref": "#convoLockStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"logBeginConvo": {
|
||||
"description": "Event indicating a convo containing the viewer was started. Can be direct or group. When a member is added to a group convo, they also get this event.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId"],
|
||||
"properties": {
|
||||
@@ -139,6 +365,7 @@
|
||||
}
|
||||
},
|
||||
"logAcceptConvo": {
|
||||
"description": "Event indicating the viewer accepted a convo, and it can be moved out of the request inbox. Can be direct or group.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId"],
|
||||
"properties": {
|
||||
@@ -147,6 +374,7 @@
|
||||
}
|
||||
},
|
||||
"logLeaveConvo": {
|
||||
"description": "Event indicating the viewer left a convo. Can be direct or group.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId"],
|
||||
"properties": {
|
||||
@@ -155,6 +383,7 @@
|
||||
}
|
||||
},
|
||||
"logMuteConvo": {
|
||||
"description": "Event indicating the viewer muted a convo. Can be direct or group.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId"],
|
||||
"properties": {
|
||||
@@ -163,6 +392,7 @@
|
||||
}
|
||||
},
|
||||
"logUnmuteConvo": {
|
||||
"description": "Event indicating the viewer unmuted a convo. Can be direct or group.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId"],
|
||||
"properties": {
|
||||
@@ -171,6 +401,7 @@
|
||||
}
|
||||
},
|
||||
"logCreateMessage": {
|
||||
"description": "Event indicating a user-originated message was created. Is not emitted for system messages.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message"],
|
||||
"properties": {
|
||||
@@ -183,6 +414,7 @@
|
||||
}
|
||||
},
|
||||
"logDeleteMessage": {
|
||||
"description": "Event indicating a user-originated message was deleted. Is not emitted for system messages.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message"],
|
||||
"properties": {
|
||||
@@ -195,6 +427,7 @@
|
||||
}
|
||||
},
|
||||
"logReadMessage": {
|
||||
"description": "DEPRECATED: use logReadConvo instead. Event indicating a convo was read up to a certain message.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message"],
|
||||
"properties": {
|
||||
@@ -202,11 +435,12 @@
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"type": "union",
|
||||
"refs": ["#messageView", "#deletedMessageView"]
|
||||
"refs": ["#messageView", "#deletedMessageView", "#systemMessageView"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"logAddReaction": {
|
||||
"description": "Event indicating a reaction was added to a message.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message", "reaction"],
|
||||
"properties": {
|
||||
@@ -220,6 +454,7 @@
|
||||
}
|
||||
},
|
||||
"logRemoveReaction": {
|
||||
"description": "Event indicating a reaction was removed from a message.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message", "reaction"],
|
||||
"properties": {
|
||||
@@ -231,6 +466,294 @@
|
||||
},
|
||||
"reaction": { "type": "ref", "ref": "#reactionView" }
|
||||
}
|
||||
},
|
||||
"logReadConvo": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a convo was read up to a certain message.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"type": "union",
|
||||
"refs": ["#messageView", "#deletedMessageView", "#systemMessageView"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"logAddMember": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a member was added to a group convo. The member who was added gets a logBeginConvo (to create the convo) but also a logAddMember (to show the system message as the first message the user sees).",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message", "relatedProfiles"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"description": "A system message with data of type #systemMessageDataAddMember",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageView"
|
||||
},
|
||||
"relatedProfiles": {
|
||||
"description": "Profiles referred in the system message.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#profileViewBasic"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"logRemoveMember": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a member was removed from a group convo. The member who was removed gets a logLeaveConvo (to leave the convo) but not a logRemoveMember (because they already left, so can't see the system message).",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message", "relatedProfiles"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"description": "A system message with data of type #systemMessageDataRemoveMember",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageView"
|
||||
},
|
||||
"relatedProfiles": {
|
||||
"description": "Profiles referred in the system message.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#profileViewBasic"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"logMemberJoin": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a member joined a group convo via join link. The member who was added gets a logBeginConvo (to create the convo) but also a logMemberJoin (to show the system message as the first message the user sees).",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message", "relatedProfiles"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"description": "A system message with data of type #systemMessageDataMemberJoin",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageView"
|
||||
},
|
||||
"relatedProfiles": {
|
||||
"description": "Profiles referred in the system message.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#profileViewBasic"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"logMemberLeave": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a member voluntarily left a group convo. The member who was removed gets a logLeaveConvo (to leave the convo) but not a logMemberLeave (because they already left, so can't see the system message).",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message", "relatedProfiles"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"description": "A system message with data of type #systemMessageDataMemberLeave",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageView"
|
||||
},
|
||||
"relatedProfiles": {
|
||||
"description": "Profiles referred in the system message.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#profileViewBasic"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"logLockConvo": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a group convo was locked.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message", "relatedProfiles"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"description": "A system message with data of type #systemMessageDataLockConvo",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageView"
|
||||
},
|
||||
"relatedProfiles": {
|
||||
"description": "Profiles referred in the system message.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#profileViewBasic"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"logUnlockConvo": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a group convo was unlocked.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message", "relatedProfiles"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"description": "A system message with data of type #systemMessageDataUnlockConvo",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageView"
|
||||
},
|
||||
"relatedProfiles": {
|
||||
"description": "Profiles referred in the system message.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#profileViewBasic"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"logLockConvoPermanently": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a group convo was locked permanently.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message", "relatedProfiles"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"description": "A system message with data of type #systemMessageDataLockConvoPermanently",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageView"
|
||||
},
|
||||
"relatedProfiles": {
|
||||
"description": "Profiles referred in the system message.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#profileViewBasic"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"logEditGroup": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating info about group convo was edited.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"description": "A system message with data of type #systemMessageDataEditGroup",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageView"
|
||||
}
|
||||
}
|
||||
},
|
||||
"logCreateJoinLink": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a join link was created for a group convo.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"description": "A system message with data of type #systemMessageDataCreateJoinLink",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageView"
|
||||
}
|
||||
}
|
||||
},
|
||||
"logEditJoinLink": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a settings about a join link for a group convo were edited.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"description": "A system message with data of type #systemMessageDataEditJoinLink",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageView"
|
||||
}
|
||||
}
|
||||
},
|
||||
"logEnableJoinLink": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a join link was enabled for a group convo.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"description": "A system message with data of type #systemMessageDataEnableJoinLink",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageView"
|
||||
}
|
||||
}
|
||||
},
|
||||
"logDisableJoinLink": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a join link was disabled for a group convo.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "message"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"message": {
|
||||
"description": "A system message with data of type #systemMessageDataDisableJoinLink",
|
||||
"type": "ref",
|
||||
"ref": "#systemMessageView"
|
||||
}
|
||||
}
|
||||
},
|
||||
"logIncomingJoinRequest": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a join request was made to a group the viewer owns. Only the owner gets this.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "member"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"member": {
|
||||
"description": "Prospective member who requested to join.",
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#profileViewBasic"
|
||||
}
|
||||
}
|
||||
},
|
||||
"logApproveJoinRequest": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a join request was approved by the viewer. Only the owner gets this. The approved member gets a logBeginConvo.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "member"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"member": {
|
||||
"description": "Prospective member who requested to join.",
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#profileViewBasic"
|
||||
}
|
||||
}
|
||||
},
|
||||
"logRejectJoinRequest": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a join request was rejected by the viewer. Only the owner gets this.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId", "member"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" },
|
||||
"member": {
|
||||
"description": "Prospective member who requested to join.",
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#profileViewBasic"
|
||||
}
|
||||
}
|
||||
},
|
||||
"logOutgoingJoinRequest": {
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Event indicating a join request was made by the viewer.",
|
||||
"type": "object",
|
||||
"required": ["rev", "convoId"],
|
||||
"properties": {
|
||||
"rev": { "type": "string" },
|
||||
"convoId": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Marks a message as deleted for the viewer, so they won't see that message in future enumerations.",
|
||||
"errors": [
|
||||
{ "name": "InvalidConvo" },
|
||||
{
|
||||
"name": "MessageDeleteNotAllowed",
|
||||
"description": "Indicates that this message cannot be deleted, e.g. because it is a system message."
|
||||
}
|
||||
],
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Gets an existing conversation by its ID.",
|
||||
"errors": [{ "name": "InvalidConvo" }],
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"required": ["convoId"],
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Get whether the requester and the other members can chat. If an existing convo is found for these members, it is returned.",
|
||||
"description": "Check whether the requester and the other members can start a 1-1 chat. Only applicable to direct (non-group) conversations. If an existing convo is found for these members, it is returned. Does not create a new convo if it doesn't exist.",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"required": ["members"],
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Get or create a 1-1 conversation for the given members. Always returns the same direct (non-group) conversation. To create a group conversation, use createGroup.",
|
||||
"errors": [
|
||||
{ "name": "AccountSuspended" },
|
||||
{ "name": "BlockedActor" },
|
||||
{ "name": "MessagesDisabled" },
|
||||
{ "name": "NotFollowedBySender" },
|
||||
{ "name": "RecipientNotFound" }
|
||||
],
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"required": ["members"],
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "chat.bsky.convo.getConvoMembers",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Returns a paginated list of members from a conversation.",
|
||||
"errors": [{ "name": "InvalidConvo" }],
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"required": ["convoId"],
|
||||
"properties": {
|
||||
"convoId": { "type": "string" },
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": 50
|
||||
},
|
||||
"cursor": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["members"],
|
||||
"properties": {
|
||||
"cursor": { "type": "string" },
|
||||
"members": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#profileViewBasic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,30 @@
|
||||
"chat.bsky.convo.defs#logCreateMessage",
|
||||
"chat.bsky.convo.defs#logDeleteMessage",
|
||||
"chat.bsky.convo.defs#logReadMessage",
|
||||
|
||||
"chat.bsky.convo.defs#logAddReaction",
|
||||
"chat.bsky.convo.defs#logRemoveReaction"
|
||||
"chat.bsky.convo.defs#logRemoveReaction",
|
||||
"chat.bsky.convo.defs#logReadConvo",
|
||||
|
||||
"chat.bsky.convo.defs#logAddMember",
|
||||
"chat.bsky.convo.defs#logRemoveMember",
|
||||
"chat.bsky.convo.defs#logMemberJoin",
|
||||
"chat.bsky.convo.defs#logMemberLeave",
|
||||
|
||||
"chat.bsky.convo.defs#logLockConvo",
|
||||
"chat.bsky.convo.defs#logUnlockConvo",
|
||||
"chat.bsky.convo.defs#logLockConvoPermanently",
|
||||
"chat.bsky.convo.defs#logEditGroup",
|
||||
|
||||
"chat.bsky.convo.defs#logCreateJoinLink",
|
||||
"chat.bsky.convo.defs#logEditJoinLink",
|
||||
"chat.bsky.convo.defs#logEnableJoinLink",
|
||||
"chat.bsky.convo.defs#logDisableJoinLink",
|
||||
|
||||
"chat.bsky.convo.defs#logIncomingJoinRequest",
|
||||
"chat.bsky.convo.defs#logApproveJoinRequest",
|
||||
"chat.bsky.convo.defs#logRejectJoinRequest",
|
||||
"chat.bsky.convo.defs#logOutgoingJoinRequest"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Returns a page of messages from a conversation.",
|
||||
"errors": [{ "name": "InvalidConvo" }],
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"required": ["convoId"],
|
||||
@@ -31,9 +33,18 @@
|
||||
"type": "union",
|
||||
"refs": [
|
||||
"chat.bsky.convo.defs#messageView",
|
||||
"chat.bsky.convo.defs#deletedMessageView"
|
||||
"chat.bsky.convo.defs#deletedMessageView",
|
||||
"chat.bsky.convo.defs#systemMessageView"
|
||||
]
|
||||
}
|
||||
},
|
||||
"relatedProfiles": {
|
||||
"description": "Set of all members who authored or reacted to the returned messages. Members referred to by system messages are also included.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.actor.defs#profileViewBasic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Leaves a conversation (direct or group). For group, this effectively removes membership. For direct, membership is never removed, only changed to remove from enumerations by the user who left.",
|
||||
"errors": [
|
||||
{ "name": "InvalidConvo" },
|
||||
{
|
||||
"name": "OwnerCannotLeave",
|
||||
"description": "The owner of a group conversation cannot leave before locking the group."
|
||||
}
|
||||
],
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "chat.bsky.convo.listConvoRequests",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Returns a page of incoming conversation requests for the user. Direct convo requests are returned as convoView; group join requests are returned as joinRequestView.",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": 50
|
||||
},
|
||||
"cursor": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["requests"],
|
||||
"properties": {
|
||||
"cursor": { "type": "string" },
|
||||
"requests": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "union",
|
||||
"refs": [
|
||||
"chat.bsky.convo.defs#convoView",
|
||||
"chat.bsky.group.defs#joinRequestView"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "query",
|
||||
"description": "Returns a page of conversations (direct or group) for the user.",
|
||||
"parameters": {
|
||||
"type": "params",
|
||||
"properties": {
|
||||
@@ -19,8 +20,14 @@
|
||||
"knownValues": ["unread"]
|
||||
},
|
||||
"status": {
|
||||
"description": "Filter convos by their status. It is discouraged to call with \"request\" and preferred to call chat.bsky.convo.listConvoRequests, which also includes group join requests made by the user.",
|
||||
"type": "string",
|
||||
"knownValues": ["request", "accepted"]
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "Filter by conversation kind.",
|
||||
"knownValues": ["direct", "group"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "chat.bsky.convo.lockConvo",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "[NOTE: This is under active development and should be considered unstable while this note is here]. Locks a group convo so no more content (messages, reactions) can be added to it.",
|
||||
"errors": [
|
||||
{ "name": "ConvoLocked" },
|
||||
{ "name": "InvalidConvo" },
|
||||
{ "name": "InsufficientRole" }
|
||||
],
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["convoId"],
|
||||
"properties": {
|
||||
"convoId": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["convo"],
|
||||
"properties": {
|
||||
"convo": {
|
||||
"type": "ref",
|
||||
"ref": "chat.bsky.convo.defs#convoView"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Mutes a conversation, preventing notifications related to it.",
|
||||
"errors": [{ "name": "InvalidConvo" }],
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{ "name": "InvalidConvo" },
|
||||
{
|
||||
"name": "ReactionNotAllowed",
|
||||
"description": "Indicates that reactions are not allowed on this message, e.g. because it is a system message."
|
||||
},
|
||||
{
|
||||
"name": "ReactionMessageDeleted",
|
||||
"description": "Indicates that the message has been deleted and reactions can no longer be added/removed."
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "procedure",
|
||||
"description": "Sends a message to a conversation.",
|
||||
"errors": [{ "name": "ConvoLocked" }, { "name": "InvalidConvo" }],
|
||||
"input": {
|
||||
"encoding": "application/json",
|
||||
"schema": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user